OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / DocumentAndRaceTests.cs

131 lines · csharp ref:
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Interactivity;
using Avalonia.Threading;

using IFSEngine.Model;

using IFSRenderer.Shared;

using Xunit;

namespace IFSRenderer.AvaloniaShell.Tests;

/// <summary>
/// The document-handling and concurrency reports: "save crashes / save as works", and
/// "while adding nodes I somehow broke the renderer, it stopped rendering".
/// </summary>
public class DocumentAndRaceTests
{
    /// <summary>
    /// Startup loads a template from the install tree, which is a read-only squashfs mount in
    /// an AppImage. Treating it as the open document is what made Ctrl+S write into the mount.
    /// </summary>
    [AvaloniaFact]
    public void A_shipped_template_is_never_the_open_document()
    {
        string template = Path.Combine(AssetPaths.Library, "Templates", "nebula_sample.ifsjson");
        Assert.True(File.Exists(template), $"expected a template at {template}");

        Assert.True(MainWindow.IsShippedTemplate(template),
            "a file under the shipped Library/ must be treated as a template");
        Assert.False(MainWindow.IsShippedTemplate(
            Path.Combine(Path.GetTempPath(), "my-fractal.ifsjson")),
            "a file the user chose elsewhere is a document, not a template");
    }

    /// <summary>
    /// Save must never take the process down. It runs from an async void click handler, where
    /// an escaping exception is fatal, and the target can be unwritable for reasons the app
    /// does not control.
    /// </summary>
    [AvaloniaFact]
    public async Task Saving_to_an_unwritable_path_reports_instead_of_throwing()
    {
        (MainWindow window, Fakes.FakeRenderQueue queue) = MainWindowHarness.Start();

        // A directory where the file cannot be created: the path names a directory, not a file.
        string unwritable = Path.Combine(Path.GetTempPath(),
            "ifsrenderer-unwritable-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(unwritable);

        // The serializer is what the save path calls; prove the failure is a real one first.
        await Assert.ThrowsAnyAsync<Exception>(
            () => IfsNodesSerializer.SaveJsonFileAsync(queue.Current, unwritable));

        // Ctrl+S on a freshly started app is the reported crash. The template is not the
        // document, so this falls through to Save as and must not throw either way.
        var save = window.FindControl<MenuItem>("MenuSave");
        Assert.NotNull(save);
        Exception escaped = Record.Exception(() =>
        {
            save.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
            Dispatcher.UIThread.RunJobs();
        });
        Assert.Null(escaped);
    }

    /// <summary>
    /// Startup must not leave the template as the edited file — the whole point of the fix.
    /// </summary>
    [AvaloniaFact]
    public void Startup_from_a_template_leaves_no_document_to_overwrite()
    {
        (MainWindow window, _) = MainWindowHarness.Start();

        // The window titles itself after the file it loaded...
        Assert.Contains("nebula", window.Title, StringComparison.OrdinalIgnoreCase);

        // ...but the template's mtime must be untouched by a save attempt.
        string template = Path.Combine(AssetPaths.Library, "Templates", "nebula_sample.ifsjson");
        DateTime before = File.GetLastWriteTimeUtc(template);

        var save = window.FindControl<MenuItem>("MenuSave");
        save.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
        Dispatcher.UIThread.RunJobs();

        Assert.Equal(before, File.GetLastWriteTimeUtc(template));
    }

    /// <summary>
    /// The mechanism behind "adding nodes stopped the renderer".
    /// </summary>
    /// <remarks>
    /// <para>
    /// The shipped code rebuilt the UI on the line after <c>Post</c>, so the UI thread walked
    /// <c>Iterators</c> while the render thread was mutating the very same collection. This
    /// proves that pairing actually throws — which is what reached the dispatch loop, paused
    /// it, and left the app looking frozen. It is the missing link between the ordering bug and
    /// the stall, and without it the diagnosis would only be plausible.
    /// </para>
    /// <para>
    /// The ordering fix removes the pairing entirely: the rebuild is posted from inside the
    /// queued action, so it can never run alongside it.
    /// </para>
    /// </remarks>
    [Fact]
    public async Task Enumerating_iterators_while_they_are_mutated_throws()
    {
        var plugins = await Task.WhenAll(Directory
            .GetFiles(AssetPaths.Transforms, "*.ifstf", SearchOption.AllDirectories)
            .Take(2)
            .Select(TransformPlugin.FromFile));
        Assert.NotEmpty(plugins);

        var fractal = new IFS();
        for (int i = 0; i < 4; i++)
            fractal.AddIterator(new Iterator(plugins[0]), false);

        Assert.ThrowsAny<InvalidOperationException>(() =>
        {
            foreach (Iterator _ in fractal.Iterators)
                fractal.AddIterator(new Iterator(plugins[0]), false);
        });
    }
}