Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -834,13 +834,16 @@ jobs:

# Wall-clock performance tests are advisory — a slow shared runner must not
# fail the build on a timing assertion.
# Throughput measurements run one fixture at a time: the suite is otherwise parallel
# (TestParallelism.cs), and concurrent fixtures would skew each other's timings.
- name: Performance tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=Performance"
--logger "trx;LogFileName=perf-tests.trx"
-- NUnit.NumberOfTestWorkers=1

# AFTER every test leg, and it runs even when one of them failed. Ordered before them it
# masks the suite completely: this step going red stopped the job, no test ran at all, and
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ The solution uses the `.slnx` XML solution format. `Directory.Build.props` appli

## Test


Fixtures run in parallel (`Compression.Tests/TestParallelism.cs`); the tests inside one fixture
run in order. Keep every fixture independent: its own temp directory (`Guid`-named), its own
device-mapper or mount names, no fixed paths shared with another fixture, no process-wide
settings. A fixture that must change process-wide state (the current directory, an environment
variable, a global switch or observer) is marked `[NonParallelizable]` with the reason beside it.
Add `-- NUnit.NumberOfTestWorkers=1` to run the suite sequentially, as the performance step does.

### Default tier

The tier that must pass. It excludes every category that needs an external tool
Expand Down
2 changes: 1 addition & 1 deletion Compression.Tests/Erofs/ErofsExternalConformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private static bool TryFromPath(string tool, out string fullPath) {

private static bool WslAvailable => _wslAvailable.Value;

private static readonly Dictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal); // fixtures run in parallel

private static bool WslHasTool(string tool) {
if (!WslAvailable) return false;
Expand Down
2 changes: 1 addition & 1 deletion Compression.Tests/ExternalFsInteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2554,7 +2554,7 @@ internal static class FsInteropToolbox {
public static bool WslHasUfsKernelModule { get; } = DetectWslUfsModule();
public static bool WslHasAdfsKernelModule { get; } = DetectWslAdfsModule();

private static readonly Dictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal); // fixtures run in parallel

private static bool DetectWslPasswordlessSudo() {
if (!WslAvailable) return false;
Expand Down
2 changes: 1 addition & 1 deletion Compression.Tests/Fat/FatExternalConformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private static bool TryFromPath(string tool, out string fullPath) {

private static bool WslAvailable => _wslAvailable.Value;

private static readonly Dictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal); // fixtures run in parallel

private static bool WslHasTool(string tool) {
if (!WslAvailable) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private static bool TryFromPath(string tool, out string fullPath) {
});

private static bool WslAvailable => _wslAvailable.Value;
private static readonly Dictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, bool> _wslToolCache = new(StringComparer.Ordinal); // fixtures run in parallel

private static bool WslHasTool(string tool) {
if (!WslAvailable) return false;
Expand Down
1 change: 1 addition & 0 deletions Compression.Tests/Lib/InMemoryProcessingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Compression.Tests.Lib;
/// directory. Write-back is atomic (temp-file + rename).
/// </summary>
[TestFixture]
[NonParallelizable] // sets the process-wide InMemoryProcessing.ThresholdBytes
public class InMemoryProcessingTests {

[Test, Category("Spec")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Compression.Tests.Operations;
/// and we exercise it by lowering the threshold.
/// </summary>
[TestFixture]
[NonParallelizable] // sets the process-wide InMemoryProcessing.ThresholdBytes and counts every cwb_x2m_/cwb_fsconv_ temp directory
public class ConvertArchiveInMemoryTests {

// ── Helpers ────────────────────────────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions Compression.Tests/Reiser4/Reiser4KernelOracle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ namespace Compression.Tests.Reiser4;
internal static class Reiser4KernelOracle {

private static readonly string ScriptDirectory = Path.Combine(AppContext.BaseDirectory, "Reiser4", "KernelOracle");
private static bool? _available;
// Lazy: fixtures run in parallel and Prepare() may download the kernel; it must run once.
private static readonly Lazy<bool> _available = new(Prepare);

internal static void Require() {
if (!FsInteropToolbox.WslAvailable && OperatingSystem.IsWindows())
Assert.Ignore("WSL is not available on this machine.");
_available ??= Prepare();
if (_available != true)
if (!_available.Value)
Assert.Ignore("The Reiser4 kernel oracle is not prepared (set CWB_REISER4_KERNEL_DOWNLOAD=1 to fetch it; needs qemu-system-x86_64).");
}

Expand Down
7 changes: 7 additions & 0 deletions Compression.Tests/Support/DmsdosCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@ private static string CacheDir {
public static string CvfTest(string buildDir) => Path.Combine(buildDir, "cvftest");
public static string DcRead(string buildDir) => Path.Combine(buildDir, "dcread");

private static readonly object FillGate = new();

/// <summary>
/// Returns the path to a built dmsdos <c>build/</c> directory, or <c>null</c>
/// if the tools cannot be produced on this machine.
/// </summary>
public static string? EnsureTools() {
// Parallel fixtures share this cache: one fill at a time, the rest then hit the cache.
lock (FillGate) return EnsureToolsUnlocked();
}

private static string? EnsureToolsUnlocked() {
var explicitDir = Environment.GetEnvironmentVariable("CWB_DMSDOS_BUILD");
if (!string.IsNullOrEmpty(explicitDir) && File.Exists(Path.Combine(explicitDir, "cvftest")))
return explicitDir;
Expand Down
11 changes: 10 additions & 1 deletion Compression.Tests/Support/FreeDosCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ private static string CacheDir {
}
}

private static readonly object FillGate = new();

/// <summary>
/// Returns the cached path to <c>FD14LIVE.iso</c>, fetching + extracting on
/// first use. Returns <c>null</c> on any failure — callers must
Expand All @@ -64,6 +66,11 @@ private static string CacheDir {
/// </para>
/// </summary>
public static string? EnsureLiveCdIso() {
// Parallel fixtures share this cache: one fill at a time, the rest then hit the cache.
lock (FillGate) return EnsureLiveCdIsoUnlocked();
}

private static string? EnsureLiveCdIsoUnlocked() {
// 1. Honor explicit env var first — air-gapped CI / dev machines can stage
// the ISO out-of-band and skip the network round-trip.
var explicitPath = Environment.GetEnvironmentVariable("CWB_FREEDOS_ISO");
Expand All @@ -81,7 +88,9 @@ private static string CacheDir {
var bytes = _http.Value.GetByteArrayAsync(LiveCdZipUrl).GetAwaiter().GetResult();
if (!Sha256OfBytes(bytes).Equals(LiveCdZipSha256, StringComparison.OrdinalIgnoreCase))
return null; // Tampered mirror — refuse to cache a mystery blob.
File.WriteAllBytes(zipPath, bytes);
var partial = zipPath + "." + Guid.NewGuid().ToString("N") + ".partial";
File.WriteAllBytes(partial, bytes);
File.Move(partial, zipPath, overwrite: true);
} catch (HttpRequestException) {
return null; // offline / DNS / 4xx / 5xx
} catch (TaskCanceledException) {
Expand Down
11 changes: 10 additions & 1 deletion Compression.Tests/Support/RetroImageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ private static string CacheDir {
public static string? Fetch(string url, string sha256, string localName) =>
FetchInternal(url, sha256, localName, decompressGz: false);

private static readonly object FillGate = new();

/// <summary>
/// Fetches a gzipped disk image (<c>.d64.gz</c> etc.), decompresses it, verifies
/// the decompressed bytes' sha256 matches <paramref name="sha256OfDecompressed"/>,
Expand All @@ -53,6 +55,11 @@ private static string CacheDir {
FetchInternal(url, sha256OfDecompressed, localName, decompressGz: true);

private static string? FetchInternal(string url, string expectedSha256, string localName, bool decompressGz) {
// Parallel fixtures share this cache: one fill at a time, the rest then hit the cache.
lock (FillGate) return FetchInternalUnlocked(url, expectedSha256, localName, decompressGz);
}

private static string? FetchInternalUnlocked(string url, string expectedSha256, string localName, bool decompressGz) {
var localPath = Path.Combine(CacheDir, localName);
// Cache hit: trust if sha matches.
if (File.Exists(localPath) && Sha256Of(localPath).Equals(expectedSha256, StringComparison.OrdinalIgnoreCase))
Expand All @@ -73,7 +80,9 @@ private static string CacheDir {
// Hash mismatch — don't cache a mystery blob; surface as unavailable.
return null;
}
File.WriteAllBytes(localPath, bytes);
var partial = localPath + "." + Guid.NewGuid().ToString("N") + ".partial";
File.WriteAllBytes(partial, bytes);
File.Move(partial, localPath, overwrite: true);
return localPath;
} catch (HttpRequestException) {
return null; // offline, DNS, 4xx, 5xx
Expand Down
6 changes: 6 additions & 0 deletions Compression.Tests/TestParallelism.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Fixtures run in parallel; tests inside one fixture stay sequential, so fixture-level state
// (fields set up in [SetUp]/[OneTimeSetUp]) needs no locking. A fixture that touches anything
// process-wide - the current directory, environment variables, a global observer, a fixed
// temp path, sudo/device-mapper names - must either make that state its own or be marked
// [NonParallelizable] with the reason beside the attribute.
[assembly: Parallelizable(ParallelScope.Fixtures)]
Loading