Files
Libation/Source/_Tests/LibationFileManager.Tests/DiskSpaceHelperTests.cs
T
Cursor Agentandrmcrackan 16e6a6f659 bugfix #1919: resolve Unix mount points for disk-space preflight
Path.GetPathRoot always returns "/" for absolute Unix paths, so bulk
backup preflight queried composefs root free space (often 0 on Bazzite)
instead of the filesystem containing Books/In progress.

On Unix, symlink-canonicalize paths (so /home -> /var/home) and pick the
longest DriveInfo.GetDrives() mount prefix. Route GetBackupDriveSpaces
through the shared helper so grouping and free-space queries agree.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-09 17:51:36 +00:00

225 lines
7.2 KiB
C#

using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Linq;
namespace LibationFileManager.Tests;
[TestClass]
public class DiskSpaceHelperTests
{
[TestMethod]
public void IsDiskFullException_detects_message()
{
var ex = new IOException("There is not enough space on the disk. : 'C:\\temp\\x.aaxc'.");
Assert.IsTrue(DiskSpaceHelper.IsDiskFullException(ex));
}
[TestMethod]
public void IsDiskFullException_detects_message_in_aggregate()
{
var inner = new IOException("Failed to create file because the disk was full.");
var ex = new AggregateException(inner);
Assert.IsTrue(DiskSpaceHelper.IsDiskFullException(ex));
}
[TestMethod]
public void ErrorMessageIndicatesDiskFull_matches_common_phrases()
{
Assert.IsTrue(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("There is not enough space on the disk. : 'C:\\temp\\x.aaxc'."));
Assert.IsTrue(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("Write failed: disk quota has been exceeded."));
Assert.IsTrue(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("No space left on device"));
Assert.IsFalse(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("Unable to read beyond the end of the stream."));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void Windows_NormalizePathForDriveQuery_strips_extended_prefix()
{
Assert.AreEqual(@"C:\Audiobooks\Books", DiskSpaceHelper.NormalizePathForDriveQuery(@"\\?\C:\Audiobooks\Books"));
Assert.AreEqual(@"\\server\share\Books", DiskSpaceHelper.NormalizePathForDriveQuery(@"\\?\UNC\server\share\Books"));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void Windows_GetPathRootForDiskSpaceCheck_strips_extended_prefix()
{
Assert.AreEqual(@"C:\", DiskSpaceHelper.GetPathRootForDiskSpaceCheck(@"\\?\C:\Audiobooks\Books"));
}
[TestMethod]
public void FindLongestMountPointPrefix_prefers_var_home_over_root()
{
var mounts = new[] { "/", "/boot", "/var/home", "/tmp" };
Assert.AreEqual(
"/var/home",
DiskSpaceHelper.FindLongestMountPointPrefix("/var/home/user/Libation/Books", mounts));
Assert.AreEqual(
"/var/home",
DiskSpaceHelper.FindLongestMountPointPrefix("/var/home", mounts));
Assert.AreEqual(
"/tmp",
DiskSpaceHelper.FindLongestMountPointPrefix("/tmp/Libation-user", mounts));
Assert.AreEqual(
"/",
DiskSpaceHelper.FindLongestMountPointPrefix("/usr/local/bin", mounts));
}
[TestMethod]
public void FindLongestMountPointPrefix_does_not_match_partial_segment()
{
// "/var/home2/..." must not match mount "/var/home"
var mounts = new[] { "/", "/var/home" };
Assert.AreEqual(
"/",
DiskSpaceHelper.FindLongestMountPointPrefix("/var/home2/books", mounts));
}
[TestMethod]
public void FindLongestMountPointPrefix_home_without_symlink_resolution_stays_on_root()
{
// Documents the Bazzite trap: /home/... does not prefix-match /var/home until symlinks are resolved.
var mounts = new[] { "/", "/var/home", "/tmp" };
Assert.AreEqual(
"/",
DiskSpaceHelper.FindLongestMountPointPrefix("/home/user/Libation/Books", mounts));
}
[TestMethod]
public void ResolvePathSymlinks_follows_home_to_var_home_style_link()
{
if (OperatingSystem.IsWindows())
Assert.Inconclusive("Skipped because OS is Windows.");
var root = Path.Combine(Path.GetTempPath(), "libation-diskspace-" + Guid.NewGuid().ToString("N"));
var varHome = Path.Combine(root, "var", "home");
var homeLink = Path.Combine(root, "home");
var booksUnderHome = Path.Combine(homeLink, "user", "Libation", "Books");
try
{
Directory.CreateDirectory(varHome);
Directory.CreateSymbolicLink(homeLink, varHome);
var resolved = DiskSpaceHelper.ResolvePathSymlinks(booksUnderHome);
var expected = Path.Combine(varHome, "user", "Libation", "Books");
Assert.AreEqual(expected, resolved);
var mounts = new[] { "/", varHome, Path.Combine(root, "tmp") };
Assert.AreEqual(
varHome,
DiskSpaceHelper.FindLongestMountPointPrefix(resolved, mounts));
}
finally
{
try
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
catch
{
// best-effort cleanup
}
}
}
[TestMethod]
public void GetPathRootForDiskSpaceCheck_unix_uses_real_mount_not_always_slash()
{
if (OperatingSystem.IsWindows())
Assert.Inconclusive("Skipped because OS is Windows.");
// Pick any ready non-root mount that exists on this machine (often /tmp or similar).
var nonRootMount = DriveInfo.GetDrives()
.Where(d => d.IsReady && d.Name is not "/" and not null)
.OrderByDescending(d => d.Name.Length)
.FirstOrDefault();
if (nonRootMount is null)
Assert.Inconclusive("No non-root mounts available to assert against.");
var probePath = Path.Combine(nonRootMount.Name.TrimEnd(Path.DirectorySeparatorChar), "libation-probe");
var root = DiskSpaceHelper.GetPathRootForDiskSpaceCheck(probePath);
Assert.AreEqual(nonRootMount.Name, root);
Assert.AreNotEqual("/", root);
}
[TestMethod]
public void GetRequiredBytesForDriveUsage_uses_batch_for_books_only()
{
const int bookCount = 500;
var expected = bookCount * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup;
Assert.AreEqual(expected, DiskSpaceHelper.GetRequiredBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.Books, bookCount));
Assert.AreEqual(
expected,
DiskSpaceHelper.GetRequiredBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.Books | DiskSpaceHelper.BackupDriveUsage.InProgress, bookCount));
}
[TestMethod]
public void GetRequiredBytesForDriveUsage_uses_single_title_for_in_progress_only()
{
Assert.AreEqual(
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.GetRequiredBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.InProgress, 500));
}
[TestMethod]
public void GetCriticalFreeBytesForDriveUsage_in_progress_requires_one_title_plus_margin()
{
Assert.AreEqual(
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup + DiskSpaceHelper.InProgressPreflightMarginBytes,
DiskSpaceHelper.GetCriticalFreeBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.InProgress));
Assert.AreEqual(
DiskSpaceHelper.CriticalFreeBytes,
DiskSpaceHelper.GetCriticalFreeBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.Books));
}
[TestMethod]
public void HasSufficientSpaceForBulkBackup_in_progress_not_judged_by_batch_estimate()
{
var drives = new[]
{
new DiskSpaceHelper.BackupDriveSpace(
@"D:\",
[@"D:\Books"],
500L * 1024 * 1024 * 1024,
500L * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.BackupDriveUsage.Books),
new DiskSpaceHelper.BackupDriveSpace(
@"C:\",
[@"C:\Temp\Libation-user"],
600L * 1024 * 1024,
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.BackupDriveUsage.InProgress),
};
Assert.IsTrue(DiskSpaceHelper.HasSufficientSpaceForBulkBackup(drives));
}
[TestMethod]
public void AnyDriveCriticallyLow_in_progress_uses_single_title_threshold()
{
var drives = new[]
{
new DiskSpaceHelper.BackupDriveSpace(
@"C:\",
[@"C:\Temp\Libation-user"],
200L * 1024 * 1024,
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.BackupDriveUsage.InProgress),
};
Assert.IsTrue(DiskSpaceHelper.AnyDriveCriticallyLow(drives));
Assert.IsFalse(DiskSpaceHelper.HasSufficientSpaceForBulkBackup(drives));
}
}