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++; } } }