OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / LegacyPaletteTests.cs

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

using IFSEngine.Model;

using IFSRenderer.Shared;

using Xunit;

namespace IFSRenderer.AvaloniaShell.Tests;

/// <summary>
/// Documents saved before the ColorPalette change must still render.
/// </summary>
/// <remarks>
/// <para>
/// The engine replaced <c>FlamePalette</c> (a flat array of baked samples under <c>Colors</c>)
/// with <c>ColorPalette</c> (key colours under <c>KeyColors</c>). Every .ifsjson written before
/// that carries the old shape — including the templates the app ships and loads at startup — and
/// the new model ignores it silently: the gradient deserializes EMPTY, and
/// <c>SampleGradient</c> then returns the background colour at every position, so the fractal
/// renders flat. No exception, no warning, just a broken picture.
/// </para>
/// <para>
/// That is a compatibility break for every file any user has ever saved, so it is worth a test
/// rather than a comment.
/// </para>
/// </remarks>
public class LegacyPaletteTests
{
    private static string TemplatePath =>
        Path.Combine(AssetPaths.Library, "Templates", "nebula_sample.ifsjson");

    /// <summary>Loads the template with the real plugin library, as the app does.</summary>
    private static async Task<IFS> LoadTemplate()
    {
        var transforms = await Task.WhenAll(Directory
            .GetFiles(AssetPaths.Transforms, "*.ifstf", SearchOption.AllDirectories)
            .Select(TransformPlugin.FromFile));
        return await IfsNodesSerializer.LoadJsonFileAsync(
            TemplatePath, transforms, [], ignorePluginVersions: true);
    }

    [Fact]
    public void The_shipped_template_still_carries_the_old_palette_shape()
    {
        // If this ever fails, upstream has migrated the templates and the shim below is only
        // needed for user files.
        string json = File.ReadAllText(TemplatePath);
        Assert.Contains("\"Colors\"", json);
        Assert.DoesNotContain("\"KeyColors\"", json);
    }

    [Fact]
    public async Task A_legacy_document_loads_with_a_usable_gradient()
    {
        Assert.True(File.Exists(TemplatePath), $"expected a template at {TemplatePath}");
        IFS fractal = await LoadTemplate();

        Assert.NotNull(fractal.Palette);
        Assert.True(fractal.Palette.KeyColors.Count > 1,
            $"the palette upgraded to {fractal.Palette.KeyColors.Count} key colour(s); a legacy "
            + "document's gradient was lost, so the fractal will render flat");
    }

    [Fact]
    public async Task The_upgraded_gradient_is_not_a_flat_colour()
    {
        IFS fractal = await LoadTemplate();

        // Sampling across the gradient must yield more than one distinct colour — the exact
        // failure mode is a palette that answers every position with the same value.
        var samples = Enumerable.Range(0, 16)
            .Select(i => fractal.Palette.SampleGradient(i / 15.0))
            .Select(v => (v.X, v.Y, v.Z))
            .Distinct()
            .ToList();

        Assert.True(samples.Count > 1,
            "every position on the gradient returned the same colour, which is what a lost "
            + "legacy palette looks like");
    }

    [Fact]
    public async Task The_upgraded_gradient_spans_the_full_range()
    {
        IFS fractal = await LoadTemplate();

        // Key positions must run 0..1, or part of the gradient is unreachable by ColorIndex.
        Assert.Equal(0.0, fractal.Palette.KeyColors.Keys.First(), 6);
        Assert.Equal(1.0, fractal.Palette.KeyColors.Keys.Last(), 6);
    }
}