Skip to content

Commit

Permalink
Fix symbolic link tests
Browse files Browse the repository at this point in the history
Use IDisposable and `File.Delete` to ensure that symlinks get created
and cleaned up properly on all platforms. If all symlinks aren't
deleted, the later test cleanup phases have problems deleting them.

Fixes dotnet#51502
  • Loading branch information
agocke committed Apr 22, 2021
1 parent 32d1a25 commit 416bc26
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ public class AppHostUsedWithSymbolicLinks : IClassFixture<AppHostUsedWithSymboli
{
private SharedTestState sharedTestState;

private void CreateSymbolicLink(string source, string target)
{
if (!SymbolicLinking.MakeSymbolicLink(source, target, out var errorString))
throw new Exception($"Failed to create symbolic link '{source}' targeting: '{target}': {errorString}");
}

public AppHostUsedWithSymbolicLinks(AppHostUsedWithSymbolicLinks.SharedTestState fixture)
{
sharedTestState = fixture;
Expand All @@ -44,7 +38,7 @@ public void Run_apphost_behind_symlink(string symlinkRelativePath)
Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(symlinkRelativePath)));
var symlinkFullPath = Path.Combine(testDir, symlinkRelativePath);

CreateSymbolicLink(symlinkFullPath, appExe);
using var symlink = new SymLink(symlinkFullPath, appExe);
Command.Create(symlinkFullPath)
.CaptureStdErr()
.CaptureStdOut()
Expand All @@ -66,52 +60,47 @@ public void Run_apphost_behind_transitive_symlinks(string firstSymlinkRelativePa

var fixture = sharedTestState.StandaloneAppFixture_Published
.Copy();

var appExe = fixture.TestProject.AppExe;
var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();

// second symlink -> apphost
string secondSymbolicLink = Path.Combine(testDir, secondSymlinkRelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(secondSymbolicLink));
CreateSymbolicLink(secondSymbolicLink, appExe);
string symlink2Path = Path.Combine(testDir, secondSymlinkRelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(symlink2Path));
using var symlink2 = new SymLink(symlink2Path, appExe);

// first symlink -> second symlink
string firstSymbolicLink = Path.Combine(testDir, firstSymlinkRelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(firstSymbolicLink));
CreateSymbolicLink(firstSymbolicLink, secondSymbolicLink);
string symlink1Path = Path.Combine(testDir, firstSymlinkRelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(symlink1Path));
using var symlink1 = new SymLink(symlink1Path, symlink2Path);

Command.Create(firstSymbolicLink)
Command.Create(symlink1.SrcPath)
.CaptureStdErr()
.CaptureStdOut()
.Execute()
.Should().Pass()
.And.HaveStdOutContaining("Hello World");
}

//[Theory]
//[InlineData("a/b/SymlinkToFrameworkDependentApp")]
//[InlineData("a/SymlinkToFrameworkDependentApp")]
[Fact(Skip = "Currently failing in OSX with \"No such file or directory\" when running Command.Create. " +
"CI failing to use stat on symbolic links on Linux (permission denied).")]
public void Run_framework_dependent_app_behind_symlink(/* string symlinkRelativePath */)
[Theory]
[InlineData("a/b/SymlinkToFrameworkDependentApp")]
[InlineData("a/SymlinkToFrameworkDependentApp")]
public void Run_framework_dependent_app_behind_symlink(string symlinkRelativePath)
{
// Creating symbolic links requires administrative privilege on Windows, so skip test.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return;

string symlinkRelativePath = string.Empty;

var fixture = sharedTestState.FrameworkDependentAppFixture_Published
.Copy();

var appExe = fixture.TestProject.AppExe;
var builtDotnet = fixture.BuiltDotnet.BinPath;
var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(symlinkRelativePath)));
var symlinkFullPath = Path.Combine(testDir, symlinkRelativePath);

CreateSymbolicLink(symlinkFullPath, appExe);
Command.Create(symlinkFullPath)
using var symlink = new SymLink(Path.Combine(testDir, symlinkRelativePath), appExe);
Command.Create(symlink.SrcPath)
.CaptureStdErr()
.CaptureStdOut()
.EnvironmentVariable("DOTNET_ROOT", builtDotnet)
Expand All @@ -121,8 +110,7 @@ public void Run_framework_dependent_app_behind_symlink(/* string symlinkRelative
.And.HaveStdOutContaining("Hello World");
}

[Fact(Skip = "Currently failing in OSX with \"No such file or directory\" when running Command.Create. " +
"CI failing to use stat on symbolic links on Linux (permission denied).")]
[Fact]
public void Run_framework_dependent_app_with_runtime_behind_symlink()
{
// Creating symbolic links requires administrative privilege on Windows, so skip test.
Expand All @@ -137,9 +125,9 @@ public void Run_framework_dependent_app_with_runtime_behind_symlink()
var dotnetSymlink = Path.Combine(testDir, "dotnet");
var dotnetDir = fixture.BuiltDotnet.BinPath;

CreateSymbolicLink(dotnetSymlink, dotnetDir);
using var symlink = new SymLink(dotnetSymlink, dotnetDir);
Command.Create(appExe)
.EnvironmentVariable("DOTNET_ROOT", dotnetSymlink)
.EnvironmentVariable("DOTNET_ROOT", symlink.SrcPath)
.CaptureStdErr()
.CaptureStdOut()
.Execute()
Expand All @@ -162,7 +150,7 @@ public void Put_app_directory_behind_symlink()
var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
Directory.Move(binDir, binDirNewPath);

CreateSymbolicLink(binDir, binDirNewPath);
using var symlink = new SymLink(binDir, binDirNewPath);
Command.Create(appExe)
.CaptureStdErr()
.CaptureStdOut()
Expand All @@ -186,8 +174,8 @@ public void Put_dotnet_behind_symlink()
var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
var dotnetSymlink = Path.Combine(testDir, "dotnet");

CreateSymbolicLink(dotnetSymlink, dotnetExe);
Command.Create(dotnetSymlink, fixture.TestProject.AppDll)
using var symlink = new SymLink(dotnetSymlink, dotnetExe);
Command.Create(symlink.SrcPath, fixture.TestProject.AppDll)
.CaptureStdErr()
.CaptureStdOut()
.Execute()
Expand All @@ -210,7 +198,7 @@ public void Put_app_directory_behind_symlink_and_use_dotnet()
var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
Directory.Move(binDir, binDirNewPath);

CreateSymbolicLink(binDir, binDirNewPath);
using var symlink = new SymLink(binDir, binDirNewPath);
dotnet.Exec(fixture.TestProject.AppDll)
.CaptureStdErr()
.CaptureStdOut()
Expand All @@ -231,10 +219,10 @@ public void Put_app_directory_behind_symlink_and_use_dotnet_run()

var dotnet = fixture.SdkDotnet;
var binDir = fixture.TestProject.OutputDirectory;
var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
Directory.Move(binDir, binDirNewPath);

CreateSymbolicLink(binDir, binDirNewPath);
using var symlink = new SymLink(binDir, binDirNewPath);
dotnet.Exec("run")
.WorkingDirectory(fixture.TestProject.Location)
.CaptureStdErr()
Expand Down Expand Up @@ -264,12 +252,12 @@ public void Put_satellite_assembly_behind_symlink()
var firstSatelliteDir = Directory.GetDirectories(binDir).Single(dir => dir.Contains("kn-IN"));
var firstSatelliteNewDir = Path.Combine(satellitesDir, "kn-IN");
Directory.Move(firstSatelliteDir, firstSatelliteNewDir);
CreateSymbolicLink(firstSatelliteDir, firstSatelliteNewDir);
using var symlink1 = new SymLink(firstSatelliteDir, firstSatelliteNewDir);

var secondSatelliteDir = Directory.GetDirectories(binDir).Single(dir => dir.Contains("ta-IN"));
var secondSatelliteNewDir = Path.Combine(satellitesDir, "ta-IN");
Directory.Move(secondSatelliteDir, secondSatelliteNewDir);
CreateSymbolicLink(secondSatelliteDir, secondSatelliteNewDir);
using var symlink2 = new SymLink(secondSatelliteDir, secondSatelliteNewDir);

Command.Create(appExe)
.CaptureStdErr()
Expand Down
70 changes: 43 additions & 27 deletions src/installer/tests/TestUtils/SymbolicLinking.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,79 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Runtime.InteropServices;

#nullable enable

namespace Microsoft.DotNet.CoreSetup.Test
{
public static class SymbolicLinking
public sealed class SymLink : IDisposable
{
static class Kernel32
public string SrcPath { get; private set; }
public SymLink(string src, string dest)
{
[Flags]
internal enum SymbolicLinkFlag
if (!MakeSymbolicLink(src, dest, out var errorMessage))
{
IsFile = 0x0,
IsDirectory = 0x1,
AllowUnprivilegedCreate = 0x2
throw new IOException($"Error creating symbolic link at {src} pointing to {dest}: {errorMessage}");
}

[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CreateSymbolicLink(
string symbolicLinkName,
string targetFileName,
SymbolicLinkFlag flags);
SrcPath = src;
}

static class libc
public void Dispose()
{
[DllImport("libc", SetLastError = true)]
internal static extern int symlink(
string targetFileName,
string linkPath);

[DllImport("libc", CharSet = CharSet.Ansi)]
internal static extern IntPtr strerror(int errnum);
if (SrcPath is not null)
{
File.Delete(SrcPath);
SrcPath = null!;
}
}

public static bool MakeSymbolicLink(string symbolicLinkName, string targetFileName, out string errorMessage)
private static bool MakeSymbolicLink(string symbolicLinkName, string targetFileName, out string errorMessage)
{
errorMessage = string.Empty;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!Kernel32.CreateSymbolicLink(symbolicLinkName, targetFileName, Kernel32.SymbolicLinkFlag.IsFile))
if (!CreateSymbolicLink(symbolicLinkName, targetFileName, SymbolicLinkFlag.IsFile))
{
int errno = Marshal.GetLastWin32Error();
errorMessage = $"CreateSymbolicLink failed with error number {errno}";
return false;
}
}
else
else
{
if (libc.symlink(targetFileName, symbolicLinkName) == -1)
if (symlink(targetFileName, symbolicLinkName) == -1)
{
int errno = Marshal.GetLastWin32Error();
errorMessage = Marshal.PtrToStringAnsi(libc.strerror(errno));
return false;
errorMessage = Marshal.PtrToStringAnsi(strerror(errno))!;
return false;
}
}

return true;
}

[Flags]
private enum SymbolicLinkFlag
{
IsFile = 0x0,
IsDirectory = 0x1,
AllowUnprivilegedCreate = 0x2
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CreateSymbolicLink(
string symbolicLinkName,
string targetFileName,
SymbolicLinkFlag flags);

[DllImport("libc", SetLastError = true)]
private static extern int symlink(
string targetFileName,
string linkPath);

[DllImport("libc", CharSet = CharSet.Ansi)]
private static extern IntPtr strerror(int errnum);
}
}

0 comments on commit 416bc26

Please sign in to comment.