using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using XFESystem.Browser;
using XFESystem.Core;
using XFESystem.Desktop;
using Xunit;
using Xunit.Abstractions;
namespace XFESystem.Desktop.Tests;
/// Actual packaged Monaco/Jint on a dedicated host thread. The script
/// gate proves ownership isolation, not native preemption or guest timings.
public sealed class BackgroundEditorTests(ITestOutputHelper output)
{
private const string Source = "using System;\n\npublic class BackgroundWidget\n{\n public int Value = 42;\n}\n";
[Fact]
public void ActualCoreInitializationDoesNotOwnTheDesktopAndTransfersARealCSharpModel()
{
using var fixture = new Fixture();
var editor = fixture.Open("Worker.cs", Source);
fixture.WaitForCore();
Assert.True(editor.IsEditorPreparing);
Assert.Null(editor.BrowserSession);
Assert.Equal(Source, editor.Content);
Assert.Equal(0, fixture.Canvas.Presents);
Assert.DoesNotContain(fixture.Trace, s => s.Contains("|SourcePresented|", StringComparison.Ordinal));
fixture.Desktop.Render();
Assert.True(fixture.Desktop.LastFrameCompleted);
string sourceFrame = Assert.Single(fixture.Trace, s => s.Contains("|SourcePresented|", StringComparison.Ordinal));
Assert.Contains("|SourceCharacters|" + Source.Length + "|", sourceFrame, StringComparison.Ordinal);
Assert.Contains("|VisibleNonEmptyLines|5|", sourceFrame, StringComparison.Ordinal);
Assert.Contains("|SourceFingerprint|" + Fingerprint(Source) + "|", sourceFrame, StringComparison.Ordinal);
Assert.DoesNotContain(fixture.Trace, s => s.StartsWith("XFE_EDITOR_BACKGROUND|Ready|", StringComparison.Ordinal));
fixture.Desktop.ShowDesktop();
fixture.Desktop.AdvanceFeedback(17);
fixture.Desktop.Render();
Assert.Null(fixture.Desktop.ActiveWindow);
fixture.Desktop.ShowDesktop();
fixture.Desktop.Render();
Assert.Same(editor, fixture.Desktop.ActiveWindow);
var bounds = editor.Bounds;
int x = bounds.X + bounds.Width / 2, y = bounds.Y + 14;
fixture.Desktop.Pointer(x, y, true, timestampMs: 1000);
fixture.Desktop.Pointer(x + 27, y + 13, true, timestampMs: 1020);
fixture.Desktop.Pointer(x + 27, y + 13, false, timestampMs: 1040);
fixture.Desktop.Render();
Assert.NotEqual(bounds, editor.Bounds);
Assert.Equal(Source, editor.Content);
Assert.Single(fixture.Trace, s => s.Contains("|SourcePresented|", StringComparison.Ordinal));
Assert.False(fixture.ReleaseCore.IsSet); // All desktop operations happened with Jint still held.
fixture.ReleaseCore.Set();
fixture.WaitReady(editor);
var page = Assert.IsAssignableFrom(editor.BrowserSession);
Assert.NotEqual(fixture.UiThread, fixture.WorkerThread);
Assert.Equal(Source, page.ReadEditorText());
using var proof = JsonDocument.Parse(page.Diagnostics.MonacoProof);
Assert.Equal("csharp", proof.RootElement.GetProperty("Language").GetString());
Assert.Equal(Source, proof.RootElement.GetProperty("ModelValue").GetString());
Assert.Contains(proof.RootElement.GetProperty("ViewLines").EnumerateArray(), line => line.GetProperty("Text").GetString()!.Contains("BackgroundWidget", StringComparison.Ordinal));
Assert.True(proof.RootElement.GetProperty("ViewLines").EnumerateArray().SelectMany(line => line.GetProperty("Spans").EnumerateArray()).Select(span => span.GetProperty("Color").GetString()).Distinct().Count() > 1);
Assert.Contains(fixture.Trace, line => line.StartsWith("XFE_BROWSER_SCRIPT_END|", StringComparison.Ordinal) && line.Contains("EditorCore.js|Success|True", StringComparison.Ordinal));
string frameReady = Assert.Single(fixture.Trace, line => line.StartsWith("XFE_EDITOR_BACKGROUND|FrameReady|", StringComparison.Ordinal));
int readyPumps = int.Parse(frameReady.Split("|InitializationPumps|", StringSplitOptions.None)[1].Trim(), System.Globalization.CultureInfo.InvariantCulture);
Assert.InRange(readyPumps, 0, 7);
output.WriteLine(frameReady);
fixture.Desktop.BrowserKey(new("keydown", "/", "Slash", "/"));
fixture.Desktop.BrowserKey(new("keyup", "/", "Slash"));
fixture.Desktop.Key(DesktopKey.Save);
fixture.WaitUntil(() => fixture.Storage.ReadText(editor.Location) == "/" + Source && !editor.IsDirty);
Assert.Equal("/" + Source, fixture.Storage.ReadText(editor.Location));
Assert.All(fixture.PreparationCanvases, canvas => Assert.True(canvas.MeasureCalls > 0));
Assert.Single(fixture.PreparationCanvases, canvas => canvas.HandedOff);
Assert.Empty(fixture.WorkerErrors);
}
[Fact]
public void TwoPendingWindowsCancelAndEditedSourceIsRequeuedWithoutLoss()
{
using var fixture = new Fixture();
var first = fixture.Open("First.cs", Source);
fixture.WaitForCore();
long originalGeneration = first.EditorPreparationGeneration;
var second = fixture.Open("Second.cs", "public class Second { }");
Assert.True(second.IsEditorPreparing);
fixture.Desktop.Close(second.Id);
Assert.DoesNotContain(second, fixture.Desktop.Windows);
fixture.Desktop.Activate(first.Id);
fixture.Desktop.Key(DesktopKey.Character, '/');
fixture.Desktop.Key(DesktopKey.Save);
string latest = "/" + Source;
Assert.Equal(latest, fixture.Storage.ReadText(first.Location));
fixture.ReleaseCore.Set();
fixture.WaitReady(first);
Assert.True(first.EditorPreparationGeneration > originalGeneration);
var page = Assert.IsAssignableFrom(first.BrowserSession);
Assert.Equal(latest, first.Content);
Assert.Equal(latest, page.ReadEditorText());
Assert.Equal(latest, fixture.Storage.ReadText(first.Location));
Assert.DoesNotContain(second, fixture.Desktop.Windows);
Assert.Contains(fixture.Trace, s => s.Contains("|Superseded|Window|" + first.Id + "|", StringComparison.Ordinal));
Assert.Contains(fixture.Trace, s => s.Contains("|Cancel|Window|" + second.Id + "|", StringComparison.Ordinal));
Assert.DoesNotContain(fixture.Trace, s => s.Contains("|Ready|Window|" + second.Id + "|", StringComparison.Ordinal));
Assert.Empty(fixture.WorkerErrors);
}
[Fact]
public void RejectedWorkerNeverConstructsAFontCanvasOrRunsJintOnTheCaller()
{
using var canvas = new WindowsTextCanvas(FontPath, 1280, 800);
var resources = new NeverCanvasFactory();
var browser = new BrowserService();
var service = new BrowserEditorPreparationService(browser, new RejectScheduler(), resources);
var storage = new MemoryWorkspace(seed: false);
string path = SystemPaths.Combine(SystemPaths.Documents, "Rejected.cs");
storage.WriteText(path, Source);
var desktop = new DesktopSession(canvas, storage, browser, editorPreparation: service);
var editor = desktop.Open(DesktopApplication.Editor, path);
desktop.AdvanceFeedback(17);
desktop.Render();
Assert.False(editor.IsEditorPreparing);
Assert.Null(editor.BrowserSession);
Assert.Contains("verified background editor worker is unavailable", editor.Status, StringComparison.Ordinal);
Assert.Equal(Source, editor.Content);
desktop.Key(DesktopKey.Character, '/');
desktop.Key(DesktopKey.Save);
Assert.Equal("/" + Source, storage.ReadText(path));
Assert.Equal(0, resources.Calls);
}
[Fact]
public void WorkerFontFailureKeepsTheSourceAndTheSameWorkerCanPrepareTheNextEditor()
{
using var fixture = new Fixture(failFirstCanvas: true);
var failed = fixture.Open("Unavailable.cs", Source);
fixture.WaitCompleted(failed);
Assert.Null(failed.BrowserSession);
Assert.Equal(Source, failed.Content);
Assert.Contains("private font setup failed", failed.Status, StringComparison.Ordinal);
fixture.Desktop.Key(DesktopKey.Character, '/');
fixture.Desktop.Key(DesktopKey.Save);
Assert.Equal("/" + Source, fixture.Storage.ReadText(failed.Location));
fixture.Desktop.Close(failed.Id);
var next = fixture.Open("Recovered.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(next);
Assert.Equal(Source, Assert.IsAssignableFrom(next.BrowserSession).ReadEditorText());
Assert.Empty(fixture.WorkerErrors);
}
[Fact]
public void AbortedBasicSourceFrameIsNotReportedAndCompletedProofIsEmittedOnce()
{
var canvas = new AbortingCanvas();
var storage = new MemoryWorkspace(seed: false);
string path = SystemPaths.Combine(SystemPaths.Documents, "Yield.cs");
storage.WriteText(path, Source);
var trace = new List();
var desktop = new DesktopSession(canvas, storage, new BrowserService(), editorPreparation: new PendingService(), threadIdentity: () => 71) { Trace = trace.Add };
desktop.Open(DesktopApplication.Editor, path);
desktop.Render();
Assert.False(desktop.LastFrameCompleted);
Assert.Empty(trace);
Assert.Equal(0, canvas.Presents);
canvas.Abort = false;
desktop.Render();
Assert.True(desktop.LastFrameCompleted);
string proof = Assert.Single(trace);
Assert.Contains("|SourcePresented|", proof, StringComparison.Ordinal);
Assert.Contains("|UiThread|71|", proof, StringComparison.Ordinal);
Assert.Contains("|SourceFingerprint|" + Fingerprint(Source) + "|", proof, StringComparison.Ordinal);
desktop.Pointer(1000, 650, false);
desktop.Render();
Assert.Single(trace);
}
[Fact]
public void ActualMonacoDeferredInitializationUsesTheDesktopClockOnTheWorker()
{
using var fixture = new Fixture(sharedClock: true, initialMilliseconds: 27_457);
var editor = fixture.Open("LateClock.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
var page = Assert.IsAssignableFrom(editor.BrowserSession);
Assert.NotEmpty(fixture.WorkerPumpTimes);
Assert.All(fixture.WorkerPumpTimes, time => Assert.True(time >= 27_457));
Assert.Equal(Source, page.ReadEditorText());
var delivered = Assert.Single(fixture.PreparationCanvases, canvas => canvas.HandedOff);
Assert.True(delivered.PreparedTextCalls > 0);
long layout = page.Diagnostics.LayoutPassCount;
fixture.Desktop.Render();
Assert.Equal(layout, page.Diagnostics.LayoutPassCount);
}
[Fact]
public void UnverifiedCanvasChangeStillInvalidatesThePreparedLayout()
{
using var canvas = new WindowsTextCanvas(FontPath, 1280, 800);
var page = new BrowserService(); page.SetCanvas(canvas); page.ResizeViewport(796, 404);
page.NavigateEditor(Source, "DifferentMetrics.cs"); page.Layout(new(796, 404));
long before = page.LayoutPassCount;
using var other = new WindowsTextCanvas(FontPath, 1280, 800);
page.CompleteEditorPreparationHandoff(other, equivalentTextMetrics: false);
page.Layout(new(796, 404));
Assert.Equal(before + 1, page.LayoutPassCount);
}
[Fact]
public void OngoingWorkerPumpCannotBlockSnapshotPaintingAndSaveFlushesQueuedInputInOrder()
{
using var fixture = new Fixture();
var editor = fixture.Open("Ongoing.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
var page = Assert.IsAssignableFrom(editor.BrowserSession);
fixture.GateNextPump(); fixture.Tick();
fixture.WaitUntil(() => fixture.PumpEntered.IsSet);
fixture.Desktop.Render();
fixture.Desktop.ShowDesktop(); fixture.Desktop.Render(); Assert.Null(fixture.Desktop.ActiveWindow);
fixture.Desktop.ShowDesktop(); fixture.Desktop.Render(); Assert.Same(editor, fixture.Desktop.ActiveWindow);
fixture.Desktop.BrowserKey(new("keydown", "/", "Slash", "/"));
fixture.Desktop.BrowserKey(new("keyup", "/", "Slash"));
fixture.Desktop.Key(DesktopKey.Save);
Assert.Equal(Source, fixture.Storage.ReadText(editor.Location));
Assert.True(editor.IsDirty);
fixture.Desktop.Close(editor.Id); Assert.Contains(editor, fixture.Desktop.Windows);
Assert.Equal(Source, page.ReadEditorText());
Assert.False(fixture.ReleasePump.IsSet);
fixture.ReleasePump.Set();
fixture.WaitUntil(() => fixture.Storage.ReadText(editor.Location) == "/" + Source && !editor.IsDirty);
Assert.Equal("/" + Source, page.ReadEditorText());
fixture.Desktop.Close(editor.Id); Assert.DoesNotContain(editor, fixture.Desktop.Windows);
Assert.Empty(fixture.WorkerErrors);
}
[Fact]
public void PointerFloodCoalescesAndSavingNeverClearsEditsQueuedAfterItsFlush()
{
using var fixture = new Fixture();
var editor = fixture.Open("Ordered.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
var page = Assert.IsAssignableFrom(editor.BrowserSession);
fixture.GateNextPump(); fixture.WaitUntil(() => fixture.PumpEntered.IsSet);
for (int index = 0; index < 10_000; index++) Assert.True(page.Pointer(new("mousemove", index % 5 + 10, 10)));
Assert.Null(page.Diagnostics.Error);
fixture.Desktop.BrowserKey(new("keydown", "/", "Slash", "/"));
fixture.Desktop.Key(DesktopKey.Save);
fixture.Desktop.BrowserKey(new("keydown", "x", "KeyX", "x"));
fixture.ReleasePump.Set();
fixture.WaitUntil(() => fixture.Storage.ReadText(editor.Location) == "/" + Source && page.ReadEditorText() == "/x" + Source);
Assert.True(editor.IsDirty);
fixture.Desktop.Key(DesktopKey.Save);
fixture.WaitUntil(() => fixture.Storage.ReadText(editor.Location) == "/x" + Source && !editor.IsDirty);
Assert.Contains(fixture.Trace, line => line == "XFE_DESKTOP_SAVED|" + editor.Location + "|" + (Source.Length + 1) + "\n");
Assert.Contains(fixture.Trace, line => line == "XFE_DESKTOP_SAVED|" + editor.Location + "|" + (Source.Length + 2) + "\n");
Assert.Empty(fixture.WorkerErrors);
}
[Fact]
public void PreviewFlushesQueuedSourceAndCanOpenItsWindowDuringTheUiPoll()
{
using var fixture = new Fixture();
var editor = fixture.Open("PreviewQueued.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
fixture.GateNextPump(); fixture.WaitUntil(() => fixture.PumpEntered.IsSet);
fixture.Desktop.BrowserKey(new("keydown", "/", "Slash", "/"));
fixture.Desktop.Key(DesktopKey.Run);
Assert.Same(editor, fixture.Desktop.ActiveWindow);
fixture.ReleasePump.Set();
fixture.WaitUntil(() => fixture.Desktop.ActiveWindow?.Application == DesktopApplication.Browser);
var preview = Assert.IsType(fixture.Desktop.ActiveWindow!.BrowserSession);
Assert.Equal("/" + Source, preview.Document!.QuerySelector("code.language-csharp")!.TextContent);
Assert.Equal(Source, fixture.Storage.ReadText(editor.Location));
Assert.True(editor.IsDirty);
Assert.Empty(fixture.WorkerErrors);
}
[Fact]
public void CancellationAtTheActualCoreScriptBoundaryStopsFurtherScriptsAndDeliveryFonts()
{
using var fixture = new Fixture();
var cancelled = fixture.Open("CancelledBoundary.cs", Source);
fixture.WaitForCore();
fixture.Desktop.Close(cancelled.Id); fixture.ReleaseCore.Set();
fixture.WaitUntil(() => fixture.Trace.Any(line => line.StartsWith("XFE_EDITOR_BACKGROUND|Cancelled|", StringComparison.Ordinal)));
Assert.DoesNotContain(cancelled, fixture.Desktop.Windows);
Assert.Single(fixture.PreparationCanvases);
Assert.DoesNotContain(fixture.Trace, line => line.StartsWith("XFE_BROWSER_SCRIPT_END|", StringComparison.Ordinal) && line.Contains("EditorCore.js", StringComparison.Ordinal));
Assert.DoesNotContain(fixture.Trace, line => line.StartsWith("XFE_BROWSER_SCRIPT_BEGIN|", StringComparison.Ordinal) && line.Contains("WebsiteMonaco.js", StringComparison.Ordinal));
Assert.DoesNotContain(fixture.Trace, line => line.StartsWith("XFE_EDITOR_BACKGROUND|Failed|", StringComparison.Ordinal));
Assert.Equal(Source, fixture.Storage.ReadText(cancelled.Location));
var recovered = fixture.Open("AfterCancellation.cs", Source); fixture.WaitReady(recovered);
Assert.Equal(Source, recovered.BrowserSession!.ReadEditorText());
Assert.Empty(fixture.WorkerErrors);
}
[Theory]
[InlineData("Empty.cs", "")]
[InlineData("Spacing.cs", " \t\n\t\n")]
[InlineData("TabUnicode.cs", "// δΈζπ\r\npublic sealed class Tabbed\r\n{\r\n\tpublic int Value = 42;\r\n}\r\n")]
[InlineData("Actual.js", "const value = 42;\nconsole.log(value);\n")]
public void RealViewReadinessPreservesEmptySpacingUnicodeAndJavaScript(string name, string source)
{
using var fixture = new Fixture();
var editor = fixture.Open(name, source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
Assert.Equal(source, Assert.IsAssignableFrom(editor.BrowserSession).ReadEditorText());
Assert.Contains(fixture.Trace, line => line.StartsWith("XFE_EDITOR_BACKGROUND|FrameReady|", StringComparison.Ordinal));
Assert.DoesNotContain(fixture.Trace, line => line.StartsWith("XFE_EDITOR_BACKGROUND|Failed|", StringComparison.Ordinal));
Assert.Empty(fixture.WorkerErrors);
}
[Theory]
[InlineData("z", "KeyZ")]
[InlineData("y", "KeyY")]
[InlineData("x", "KeyX")]
[InlineData("v", "KeyV")]
public void EmptyTextControlKeysCannotBeClearedByAnEarlierSaveAcknowledgement(string key, string code)
{
using var fixture = new Fixture();
using var barrierEntered = new ManualResetEventSlim();
using var releaseBarrier = new ManualResetEventSlim();
try
{
var editor = fixture.Open("ControlRace.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
var page = Assert.IsAssignableFrom(editor.BrowserSession);
fixture.Desktop.BrowserKey(new("keydown", "/", "Slash", "/"));
fixture.Desktop.BrowserKey(new("keyup", "/", "Slash"));
fixture.WaitUntil(() => page.ReadEditorText() == "/" + Source);
fixture.WaitForWorkerIdle();
fixture.Desktop.Key(DesktopKey.Save);
Assert.True(fixture.TryQueue(() => { barrierEntered.Set(); if (!releaseBarrier.Wait(TimeSpan.FromSeconds(60))) throw new TimeoutException("Control-key test barrier timed out."); }));
Assert.True(barrierEntered.Wait(TimeSpan.FromSeconds(30)));
// The real FIFO save readback has finished, but its callback has
// not run on the UI. This control event remains behind the barrier.
Assert.True(fixture.Desktop.BrowserKey(new("keydown", key, code, Control: true)));
fixture.Tick();
Assert.Equal("/" + Source, fixture.Storage.ReadText(editor.Location));
Assert.Equal("/" + Source, page.ReadEditorText());
Assert.True(editor.IsDirty);
fixture.Desktop.Close(editor.Id); Assert.Contains(editor, fixture.Desktop.Windows);
releaseBarrier.Set();
if (key == "z") fixture.WaitUntil(() => page.ReadEditorText() == Source); // Actual Monaco undo, not a simulated model.
else fixture.WaitForWorkerIdle();
Assert.True(editor.IsDirty);
Assert.Empty(fixture.WorkerErrors);
}
finally { releaseBarrier.Set(); }
}
[Fact]
public void ClosingAReadySessionCancelsTheActualNextScriptBoundaryAndQueuedResource()
{
using var fixture = new Fixture();
var editor = fixture.Open("ReadyCancellation.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
fixture.QueueCancellationProbe();
Assert.True(fixture.ProbeEntered.Wait(TimeSpan.FromSeconds(30)));
fixture.Desktop.Close(editor.Id); Assert.DoesNotContain(editor, fixture.Desktop.Windows);
fixture.ReleaseProbe.Set();
Assert.True(fixture.ProbeDone.Wait(TimeSpan.FromSeconds(30)));
Assert.True(fixture.ProbeCancelled);
Assert.False(fixture.ProbeExecuted);
Assert.Equal(0, fixture.ProbeSecondResourceReads);
Assert.DoesNotContain(fixture.Trace, line => line.StartsWith("XFE_BROWSER_SCRIPT_END|", StringComparison.Ordinal) && line.Contains("AfterReadyCancelA.js", StringComparison.Ordinal));
Assert.Empty(fixture.WorkerErrors);
}
[Theory]
[InlineData("Alt", "AltLeft", false, true)]
[InlineData("Control", "ControlLeft", true, false)]
[InlineData("Shift", "ShiftLeft", true, true)]
[InlineData("Meta", "MetaLeft", true, true)]
[InlineData("CapsLock", "CapsLock", true, true)]
[InlineData("NumLock", "NumLock", true, true)]
[InlineData("ScrollLock", "ScrollLock", true, true)]
public void PureModifierAfterAnActualSaveCannotDirtyTheDocumentOrBlockSystemClose(string key, string code, bool control, bool alt)
{
using var fixture = new Fixture();
var editor = fixture.Open("ModifierClose.cs", Source);
fixture.WaitForCore(); fixture.ReleaseCore.Set(); fixture.WaitReady(editor);
fixture.Desktop.BrowserKey(new("keydown", "/", "Slash", "/"));
fixture.Desktop.Key(DesktopKey.Save);
fixture.WaitUntil(() => fixture.Storage.ReadText(editor.Location) == "/" + Source && !editor.IsDirty);
Assert.True(fixture.Desktop.BrowserKey(new("keydown", key, code, Shift: key == "Shift", Control: control, Alt: alt)));
Assert.False(editor.IsDirty);
fixture.Desktop.Key(DesktopKey.CloseWindow);
Assert.DoesNotContain(editor, fixture.Desktop.Windows);
Assert.Equal("/" + Source, fixture.Storage.ReadText(editor.Location));
Assert.Empty(fixture.WorkerErrors);
}
private static string FontPath => Path.Combine(AppContext.BaseDirectory, "TestAssets/Font.ttf");
private static string Fingerprint(string source)
{
ulong hash = 14695981039346656037;
foreach (char c in source) { hash = unchecked((hash ^ (byte)c) * 1099511628211); hash = unchecked((hash ^ (byte)(c >> 8)) * 1099511628211); }
return hash.ToString("x16");
}
private sealed class Fixture : IDisposable, IEditorBackgroundWorkScheduler, IEditorPreparationCanvasFactory
{
public readonly MemoryWorkspace Storage = new(seed: false);
public readonly WindowsTextCanvas Canvas = new(FontPath, 1280, 800);
public readonly ConcurrentQueue Trace = new();
public readonly ConcurrentQueue WorkerErrors = new();
public readonly ConcurrentQueue PreparationCanvases = new();
public readonly ConcurrentQueue WorkerPumpTimes = new();
public readonly ManualResetEventSlim CoreEntered = new(), ReleaseCore = new();
public readonly ManualResetEventSlim PumpEntered = new(), ReleasePump = new(true);
public readonly ManualResetEventSlim ProbeEntered = new(), ReleaseProbe = new(), ProbeDone = new();
public bool ProbeCancelled, ProbeExecuted;
public int ProbeSecondResourceReads => resources.SecondReads;
public readonly DesktopSession Desktop;
public readonly int UiThread = Environment.CurrentManagedThreadId;
public int WorkerThread;
private readonly BlockingCollection queue = new(12);
private readonly Thread worker;
private int gated;
private int failFirstCanvas;
private int gatePump;
private int outstanding;
private long now;
private BrowserService? backend;
private readonly CancellationProbeResources resources = new();
public Fixture(bool failFirstCanvas = false, bool sharedClock = false, long initialMilliseconds = 0)
{
this.failFirstCanvas = failFirstCanvas ? 1 : 0;
now = initialMilliseconds;
var browser = new BrowserService(resources: resources) { Trace = line =>
{
Trace.Enqueue(line);
if (line.StartsWith("XFE_BROWSER_SCRIPT_BEGIN|", StringComparison.Ordinal) && line.Contains("AfterReadyCancelA.js", StringComparison.Ordinal))
{ ProbeEntered.Set(); if (!ReleaseProbe.Wait(TimeSpan.FromSeconds(60))) throw new TimeoutException("Ready-cancel script boundary timed out."); }
if (line.StartsWith("XFE_BROWSER_PUMP_BEGIN|", StringComparison.Ordinal) && Environment.CurrentManagedThreadId != UiThread)
{
WorkerPumpTimes.Enqueue(long.Parse(line.Split('|')[1], System.Globalization.CultureInfo.InvariantCulture));
if (Interlocked.Exchange(ref gatePump, 0) != 0)
{ PumpEntered.Set(); if (!ReleasePump.Wait(TimeSpan.FromSeconds(60))) throw new TimeoutException("Post-ready worker gate timed out."); }
}
if (line.StartsWith("XFE_BROWSER_SCRIPT_BEGIN|", StringComparison.Ordinal) && line.Contains("EditorCore.js", StringComparison.Ordinal) && Interlocked.CompareExchange(ref gated, 1, 0) == 0)
{
WorkerThread = Environment.CurrentManagedThreadId;
if (WorkerThread == UiThread) throw new InvalidOperationException("Core evaluation ran on the desktop thread.");
CoreEntered.Set();
if (!ReleaseCore.Wait(TimeSpan.FromSeconds(60))) throw new TimeoutException("Test core gate was not released.");
}
} };
var service = new BrowserEditorPreparationService(new CaptureFactory(browser, page => backend = page), this, this, Trace.Enqueue,
monotonicMilliseconds: sharedClock ? () => Volatile.Read(ref now) : null);
Desktop = new(Canvas, Storage, browser, monotonicMilliseconds: () => now, editorPreparation: service) { Trace = Trace.Enqueue };
worker = new Thread(() =>
{
foreach (var action in queue.GetConsumingEnumerable())
{ try { action(); } catch (Exception error) { WorkerErrors.Enqueue(error); } Interlocked.Decrement(ref outstanding); }
}, 8 * 1024 * 1024) { IsBackground = true, Name = "RealMonacoHostWorker" };
worker.Start();
}
public bool TryQueue(Action work)
{
Interlocked.Increment(ref outstanding);
if (queue.TryAdd(work)) return true;
Interlocked.Decrement(ref outstanding); return false;
}
public void WaitForWorkerIdle()
{
var timer = Stopwatch.StartNew();
while (Volatile.Read(ref outstanding) != 0 && timer.Elapsed < TimeSpan.FromSeconds(60)) Thread.Sleep(5);
Assert.Equal(0, Volatile.Read(ref outstanding));
}
public void QueueCancellationProbe() => Assert.True(TryQueue(() =>
{
try { backend!.ExecuteJavaScript("for(const name of ['AfterReadyCancelA.js','AfterReadyCancelB.js']){const s=document.createElement('script');s.src='/Browser/'+name;document.head.appendChild(s)}"); }
catch (OperationCanceledException) { ProbeCancelled = true; }
ProbeExecuted = backend!.Document!.Body!.HasAttribute("data-after-close");
ProbeDone.Set();
}));
public IDesktopCanvas CreateEditorPreparationCanvas()
{
Assert.NotEqual(UiThread, Environment.CurrentManagedThreadId);
if (Interlocked.Exchange(ref failFirstCanvas, 0) != 0) throw new IOException("private font setup failed");
var canvas = new PreparationCanvas(); PreparationCanvases.Enqueue(canvas); return canvas;
}
public DesktopWindow Open(string name, string source)
{
string path = SystemPaths.Combine(SystemPaths.Documents, name); Storage.WriteText(path, source);
return Desktop.Open(DesktopApplication.Editor, path);
}
public void WaitForCore() => Assert.True(CoreEntered.Wait(TimeSpan.FromSeconds(30)), string.Join("", Trace));
public void GateNextPump() { ReleasePump.Reset(); Interlocked.Exchange(ref gatePump, 1); }
public void Tick() { now += 67; Desktop.AdvanceFeedback(now); }
public void WaitUntil(Func condition)
{
var timer = Stopwatch.StartNew();
while (!condition() && timer.Elapsed < TimeSpan.FromSeconds(60)) { Tick(); Thread.Sleep(5); }
Assert.True(condition(), string.Join("", Trace));
}
public void WaitReady(DesktopWindow window)
{
WaitCompleted(window);
Assert.NotNull(window.BrowserSession);
Desktop.Render();
}
public void WaitCompleted(DesktopWindow window)
{
var timer = Stopwatch.StartNew();
while (window.IsEditorPreparing && timer.Elapsed < TimeSpan.FromSeconds(60))
{ now += 17; Desktop.AdvanceFeedback(now); Thread.Sleep(5); }
Assert.False(window.IsEditorPreparing, string.Join("", Trace));
}
public void Dispose()
{
ReleaseCore.Set(); ReleasePump.Set(); ReleaseProbe.Set();
foreach (var window in Desktop.Windows) if (window.BrowserSession is IBackgroundEditorSession background) background.Cancel();
queue.CompleteAdding();
Assert.True(worker.Join(TimeSpan.FromSeconds(60)), "Monaco host worker did not exit.");
foreach (var canvas in PreparationCanvases) canvas.Dispose();
Canvas.Dispose(); queue.Dispose(); CoreEntered.Dispose(); ReleaseCore.Dispose(); PumpEntered.Dispose(); ReleasePump.Dispose();
ProbeEntered.Dispose(); ReleaseProbe.Dispose(); ProbeDone.Dispose();
}
}
private sealed class CaptureFactory(BrowserService factory, Action capture) : IInteractiveBrowserFactory
{
public IInteractiveBrowserSession CreatePageSession()
{ var page = (BrowserService)factory.CreatePageSession(); capture(page); return page; }
}
private sealed class CancellationProbeResources : IBrowserResourceSource
{
private readonly PackagedBrowserResources packaged = new();
public int SecondReads;
public bool TryGet(Uri address, out WebResponse response)
{
if (address.AbsolutePath is "/Browser/AfterReadyCancelA.js" or "/Browser/AfterReadyCancelB.js")
{
if (address.AbsolutePath.EndsWith("B.js", StringComparison.Ordinal)) Interlocked.Increment(ref SecondReads);
response = new(address, 200, "application/javascript", "document.body.setAttribute('data-after-close','executed');"); return true;
}
return packaged.TryGet(address, out response);
}
}
private sealed class PreparationCanvas : IDesktopWebCanvas, IEditorPreparationCanvasHandoff, IDisposable
{
private readonly WindowsTextCanvas metrics = new(FontPath, 2, 2);
private int owner = Environment.CurrentManagedThreadId;
public int Width => 2; public int Height => 2;
public long MeasureCalls => metrics.MeasureCalls;
public int PreparedTextCalls;
public bool HandedOff;
public void PrepareEditorText(string text, double fontSize, string fontFamily, int fontWeight)
{ Own(); PreparedTextCalls++; metrics.RasterizeWebText(text, fontSize, fontFamily, fontWeight); }
public bool TryCompleteEditorPreparationHandoff(IDesktopCanvas target)
{
if (target is not WindowsTextCanvas actual || actual.FontSha256 != metrics.FontSha256 || target.ScalePercent != ((IDesktopCanvas)this).ScalePercent) return false;
owner = Environment.CurrentManagedThreadId; HandedOff = true; return true;
}
private void Own() => Assert.Equal(owner, Environment.CurrentManagedThreadId);
public double MeasureWebText(string text, double size, string family, int weight) { Own(); return metrics.MeasureWebText(text, size, family, weight); }
public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double size, string family, int weight) { Own(); return metrics.RasterizeWebText(text, size, family, weight); }
public void Fill(uint rgb, int x, int y, int width, int height) => throw new InvalidOperationException("Worker drew a display fill.");
public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new InvalidOperationException("Worker drew a display line.");
public void Text(string text, uint rgb, int x, int y) => throw new InvalidOperationException("Worker drew display text.");
public void WebText(string text, uint color, double x, double y, double size, string family, int weight, double opacity = 1) => throw new InvalidOperationException("Worker painted the display.");
public void PushClip(DesktopBounds bounds) => throw new InvalidOperationException("Worker clipped the display.");
public void PopClip() => throw new InvalidOperationException("Worker clipped the display.");
public void Present() => throw new InvalidOperationException("Worker presented a display.");
public void Dispose() => metrics.Dispose();
}
private sealed class RejectScheduler : IEditorBackgroundWorkScheduler { public bool TryQueue(Action work) => false; }
private sealed class NeverCanvasFactory : IEditorPreparationCanvasFactory
{ public int Calls; public IDesktopCanvas CreateEditorPreparationCanvas() { Calls++; throw new InvalidOperationException(); } }
private sealed class PendingService : IEditorPreparationService
{
public IEditorPreparationOperation Begin(EditorPreparationRequest request) => new PendingOperation(request);
private sealed class PendingOperation(EditorPreparationRequest request) : IEditorPreparationOperation
{ public EditorPreparationRequest Request => request; public void Cancel() { } public bool TryTakeResult(out EditorPreparationResult result) { result = null!; return false; } }
}
private sealed class AbortingCanvas : IDesktopCanvas
{
public bool Abort = true; public int Presents;
public int Width => 1280; public int Height => 800;
public void Fill(uint rgb, int x, int y, int width, int height) { }
public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
public void Text(string text, uint rgb, int x, int y) { if (Abort && text.Contains("BackgroundWidget", StringComparison.Ordinal)) throw new DesktopRenderYieldException(); }
public void Present() { Presents++; }
}
}