Skip to content

Latest commit

 

History

History
369 lines (264 loc) · 11.9 KB

File metadata and controls

369 lines (264 loc) · 11.9 KB

TempDirectory

A temporary directory helper for tests that automatically cleans up directories.

Features

  • Creates unique temporary directories in %TEMP%\VerifyTempDirectory\{Path.GetRandomFileName()}
  • Automatic cleanup on dispose
  • Implicit conversion to string and DirectoryInfo
  • Thread-safe directory creation
  • Removes orphaned directories older than 24 hours

Usage

[Fact]
public void Usage()
{
    using var temp = new TempDirectory();

    // write a file to the temp directory
    File.WriteAllText(Path.Combine(temp, "test.txt"), "content");

    // Directory and files automatically deleted here
}

snippet source | anchor

Orphaned directories

Orphaned directories can occur in the following scenario

  • A breakpoint is set in a test that uses TempDirectory
  • Debugger is launched and that breakpoint is hit
  • Debugger is force stopped, resulting in the TempDirectory.Dispose() not being executed

Path Property

Contains the full path to the temporary directory.

[Fact]
public void PathProperty()
{
    using var temp = new TempDirectory();
    var path = temp.Path;
    Assert.True(Directory.Exists(path));
    Assert.True(Path.IsPathRooted(path));
}

snippet source | anchor

Implicit Conversion

Implicit Conversion is helpful as it allows a TempDirectory instance to be passed to directly to method that takes a string or a DirectoryInfo.

String Implicit Conversion

TempDirectory can be implicitly converted to a string:

[Fact]
public void StringConversion()
{
    using var temp = new TempDirectory();

    File.WriteAllText(Path.Combine(temp, "test.txt"), "content");

    // implicit conversion to string
    string path = temp;
    var files = Directory.EnumerateFiles(path);
    Trace.WriteLine(files.Count());
}

snippet source | anchor

DirectoryInfo Implicit Conversion

TempDirectory can be implicitly converted to a DirectoryInfo:

[Fact]
public void DirectoryInfoConversion()
{
    using var temp = new TempDirectory();

    File.WriteAllText(Path.Combine(temp, "test.txt"), "content");

    // implicit conversion to DirectoryInfo
    DirectoryInfo info = temp;

    var files = info.EnumerateFiles();
    Trace.WriteLine(files.Count());
}

snippet source | anchor

Info Property

TempDirectory has a convenience Info that can be used to access all the DirectoryInfo members:

[Fact]
public void InfoProperty()
{
    using var temp = new TempDirectory();

    File.WriteAllText(Path.Combine(temp, "test.txt"), "content");

    var files = temp.Info.EnumerateFiles();
    Trace.WriteLine(files.Count());

    temp.Info.CreateSubdirectory("Subdirectory");
}

snippet source | anchor

TempDirectory RootDirectory Property

Allows access to the root directory for all TempDirectory instances:

[Fact]
public void RootDirectory() =>
    // Accessing the root directory for all TempDirectory instances
    Trace.WriteLine(TempDirectory.RootDirectory);

snippet source | anchor

Ignore Locked Files

When a test creates files that may be locked by external processes (e.g., databases, file watchers), disposing the TempDirectory will throw an IOException. To handle this, pass ignoreLockedFiles: true to the constructor. Locked files will be silently skipped during disposal and cleaned up once they age out (24 hours).

[Fact]
public void IgnoreLockedFiles()
{
    var temp = new TempDirectory(ignoreLockedFiles: true);
    string path = temp;

    var filePath = Path.Combine(temp, "locked.txt");
    File.WriteAllText(filePath, "content");
    PreventDeletion(filePath, path);

    // Dispose will not throw despite the locked file
    temp.Dispose();

    // Directory still exists due to the locked file
    // It will be cleaned up once it ages out (24 hours)
    Assert.True(Directory.Exists(path));

    // Cleanup for test hygiene
    AllowDeletion(filePath, path);
    Directory.Delete(path, true);
}

snippet source | anchor

Cleanup Behavior

The dispose cleans up the current instance.

The static constructor automatically:

  1. Ensures the root directory %TEMP%\VerifyTempDirectory exists
  2. Deletes sub-directories not modified in the last 24 hours
  3. Runs once per application domain

Thread Safety

Each instance creates a unique directory using Path.GetRandomFileName(), making concurrent usage safe.

VerifyDirectory

TempDirectory is compatible with VerifyDirectory.

[Fact]
public async Task VerifyDirectoryInstance()
{
    using var directory = new TempDirectory();
    await File.WriteAllTextAsync(Path.Combine(directory, "test.txt"), "test");
    await VerifyDirectory(directory);
}

snippet source | anchor

BuildPath

Combines the TempDirectory.Path with more paths via Path.Combine.

[Fact]
public async Task BuildPath()
{
    using var temp = new TempDirectory();

    // path will be {temp.Path}/file.txt
    var path = temp.BuildPath("file.txt");

    // nestedPath will be {temp.Path}/nested/file.txt
    var nestedPath = temp.BuildPath("nested", "file.txt");

    await Verify(new
    {
        path,
        nestedPath
    });
}

snippet source | anchor

TempDirectory paths are scrubbed

[Fact]
public async Task Scrubbing()
{
    using var temp = new TempDirectory();

    await Verify(new
    {
        PropertyWithTempPath = temp,
        PropertyWithTempFilePath = temp.BuildPath("file.txt"),
        TempInStringProperty = $"The path is {temp}"
    });
}

snippet source | anchor

Result:

{
  PropertyWithTempPath: {TempDirectory},
  PropertyWithTempFilePath: {TempDirectory}\file.txt,
  TempInStringProperty: The path is {TempDirectory}
}

snippet source | anchor

Debugging

Given TempDirectory deletes its contents on test completion (even failure), it can be difficult to debug what caused the failure.

There are several approaches that can be used to inspect the contents of the temp directory.

The below should be considered temporary approaches to be used only during debugging. The code should not be committed to source control.

No Using

Omitting the using for the TempDirectory will prevent the temp directory from being deleted when the test finished.

[Fact(Explicit = true)]
public void NoUsing()
{
    //using var temp = new TempDirectory();
    var temp = new TempDirectory();

    File.WriteAllText(Path.Combine(temp, "file.txt"), "content");

    Debug.WriteLine(temp);
}

snippet source | anchor

The directory can then be manually inspected.

OpenExplorerAndDebug

Opens the temporary directory in the system file explorer and breaks into the debugger.

[Fact(Explicit = true)]
public void OpenExplorerAndDebug()
{
    using var temp = new TempDirectory();

    File.WriteAllText(Path.Combine(temp, "file.txt"), "content");

    // this is temporary debugging code and should not be commited to source control
    temp.OpenExplorerAndDebug();
}

snippet source | anchor

This method is designed to help debug tests by enabling the inspection of the contents of the temporary directory while the test is paused. It performs two actions:

  1. Opens the directory in the file explorer - Launches the system's default file explorer (Explorer on Windows, Finder on macOS) and navigates to the temporary directory
  2. Breaks into the debugger - If a debugger is already attached, execution breaks at this point. If no debugger is attached, it attempts to launch one. This prevents the directory being clean up by the TempDirectory.Dispose().

This enables examination of the directory contents at a specific point during test execution.

Supported Platforms:

  • Windows (uses explorer.exe)
  • macOS (uses open)

Throws an exception if used on a build server. Uses DiffEngine.BuildServerDetector.Detected.

Rider

For Debugger.Launch(); to work correctly in JetBrains Rider use Set Rider as the default debugger.