XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Reflection;
using XFESystem.Collections;

namespace XFESystem.Browser.Tests;

[Collection("Browser lifetime ownership")]
public sealed class BrowserOwnerReleaseTraceTests
{
    private const BindingFlags PrivateInstance=BindingFlags.Instance|BindingFlags.NonPublic;
    private static T Owner<T>(BrowserService browser,string field)where T:class=>
        Assert.IsType<T>(typeof(BrowserService).GetField(field,PrivateInstance)!.GetValue(browser));
    private static Dictionary<string,string> Fields(string record)
    {
        Assert.EndsWith("\n",record,StringComparison.Ordinal);
        var parts=record.TrimEnd('\n').Split('|');Assert.Equal(1,parts.Length%2);
        return Enumerable.Range(0,(parts.Length-1)/2).ToDictionary(index=>parts[1+index*2],index=>parts[2+index*2]);
    }
    private static string[] WeakRecords(IEnumerable<string> chunks)=>chunks.SelectMany(chunk=>chunk.Split('\n',StringSplitOptions.RemoveEmptyEntries))
        .Where(line=>line.StartsWith("XFE_BROWSER_WEAK_RELEASE|",StringComparison.Ordinal)).Select(line=>line+"\n").ToArray();
    private static void CheckReleased(WeakCollectionScope scope)
    {
        var after=scope.Lifetime;Assert.Equal(0,after.Tables);Assert.Equal(0,after.Entries);
        Assert.Equal(after.Created,after.Disposed);Assert.Equal(after.TableHandlesCreated,after.TableHandlesDisposed);
    }
    private static void CheckRecord(string record,string reason,WeakCollectionLifetime? before=null,WeakCollectionLifetime? after=null)
    {
        var fields=Fields(record);Assert.Equal("0",fields["TablesAfter"]);Assert.Equal("0",fields["EntriesAfter"]);
        Assert.Equal(fields["Created"],fields["Disposed"]);Assert.Equal(fields["OwnerHandlesCreated"],fields["OwnerHandlesDisposed"]);
        Assert.Equal(reason,fields["Reason"]);Assert.True(long.Parse(fields["Page"],CultureInfo.InvariantCulture)>0);
        if(before is not null){Assert.Equal(before.Tables.ToString(CultureInfo.InvariantCulture),fields["TablesBefore"]);Assert.Equal(before.Entries.ToString(CultureInfo.InvariantCulture),fields["EntriesBefore"]);}
        if(after is not null){Assert.Equal(after.Created.ToString(CultureInfo.InvariantCulture),fields["Created"]);Assert.Equal(after.TableHandlesCreated.ToString(CultureInfo.InvariantCulture),fields["OwnerHandlesCreated"]);}
    }
    private static void AllocateOwners(BrowserService browser)
    {
        browser.Navigate("about:Start");
        Assert.Equal("ready",browser.ExecuteJavaScript("globalThis.kept=document.createElement('button');document.body.append(kept);globalThis.map=new WeakMap([[kept,{node:kept}]]);setInterval(()=>map.has(kept),1000);'ready'"));
    }

    [Fact]
    public void SuccessfulReleaseReportsActualEngineAndDomCountersAtomicallyAndRepeatedReleaseAddsNothing()
    {
        var packets=new List<string>();var browser=new BrowserService{Trace=packets.Add};AllocateOwners(browser);
        var dom=Owner<DomBridge>(browser,"dom");var runtime=Owner<JavaScriptRuntime>(browser,"runtime");
        var scope=((IWeakCollectionOwner)(object)runtime.Engine).WeakCollections;var before=scope.Lifetime;
        long domBefore=dom.Lifetime.OutstandingHandles;packets.Clear();browser.Release();
        CheckReleased(scope);Assert.Equal(0,runtime.PendingTaskCount);Assert.Equal(0,dom.Lifetime.OutstandingHandles);
        Assert.Null(browser.Document);Assert.Null(browser.ReadWeakCollectionLifetime());Assert.Null(browser.ReadLifetimeDiagnostics());
        var packet=Assert.Single(packets);Assert.StartsWith("XFE_BROWSER_RELEASE|HandlesBefore|"+domBefore+"|HandlesAfter|0\n",packet,StringComparison.Ordinal);
        CheckRecord(Assert.Single(WeakRecords(packets)),"Release",before,scope.Lifetime);
        browser.Release();Assert.Single(packets);GC.KeepAlive(runtime);GC.KeepAlive(dom);
    }

    [Fact]
    public void NavigationReplacementReleasesOnlyOldOwnersAndReportsBothActualGenerations()
    {
        var packets=new List<string>();var browser=new BrowserService{Trace=packets.Add};AllocateOwners(browser);
        var oldRuntime=Owner<JavaScriptRuntime>(browser,"runtime");var oldScope=((IWeakCollectionOwner)(object)oldRuntime.Engine).WeakCollections;
        var oldBefore=oldScope.Lifetime;packets.Clear();var replacement=browser.Navigate("about:Start");
        Assert.Contains("JavaScript ready: 12",replacement.Text,StringComparison.Ordinal);CheckReleased(oldScope);
        var replacementRuntime=Owner<JavaScriptRuntime>(browser,"runtime");Assert.NotSame(oldRuntime,replacementRuntime);
        var nextScope=((IWeakCollectionOwner)(object)replacementRuntime.Engine).WeakCollections;
        Assert.True(nextScope.Lifetime.Entries>0);CheckRecord(Assert.Single(WeakRecords(packets)),"Replace",oldBefore,oldScope.Lifetime);
        var nextBefore=nextScope.Lifetime;browser.Release();CheckReleased(nextScope);
        var records=WeakRecords(packets);Assert.Equal(2,records.Length);CheckRecord(records[1],"Release",nextBefore,nextScope.Lifetime);
        Assert.Equal(Fields(records[0])["Page"],Fields(records[1])["Page"]);GC.KeepAlive(oldRuntime);
    }

    [Fact]
    public void FailureAfterReplacingOldPageReportsPartialNewRuntimeReleaseExactlyOnce()
    {
        var packets=new List<string>();var browser=new BrowserService(new TwoScriptsTransport(),new BrowserLimits{MaximumScripts=1}){Trace=packets.Add};
        AllocateOwners(browser);var oldRuntime=Owner<JavaScriptRuntime>(browser,"runtime");
        var oldScope=((IWeakCollectionOwner)(object)oldRuntime.Engine).WeakCollections;packets.Clear();
        var page=browser.Navigate("http://failure.test/TwoScripts");Assert.Equal("Navigation failed",page.Title);Assert.Null(browser.Document);
        CheckReleased(oldScope);var records=WeakRecords(packets);Assert.Equal(2,records.Length);
        CheckRecord(records[0],"Replace");CheckRecord(records[1],"NavigationFailed");
        browser.Release();Assert.Equal(2,WeakRecords(packets).Length);
        Assert.Contains("JavaScript ready: 12",browser.Navigate("about:Start").Text,StringComparison.Ordinal);browser.Release();
    }

    [Theory]
    [InlineData("Release")]
    [InlineData("NavigationFailed")]
    [InlineData("Replace")]
    public void ThrowingDiagnosticObserverCannotInterruptActualOwnerCleanupOrLaterNavigation(string reason)
    {
        bool throwOnRelease=false;int observed=0;BrowserService? browser=null;WeakCollectionScope? oldScope=null;
        string? observedPacket=null;WeakCollectionLifetime? observedScope=null;bool noOwnersAtCallback=false;
        browser=new(){Trace=packet=>
        {
            if(!throwOnRelease||!packet.StartsWith("XFE_BROWSER_RELEASE|",StringComparison.Ordinal))return;
            observed++;observedPacket=packet;observedScope=oldScope?.Lifetime;
            noOwnersAtCallback=browser!.ReadWeakCollectionLifetime() is null&&browser.ReadLifetimeDiagnostics() is null;
            throw new IOException("Controlled diagnostic observer failure.");
        }};
        AllocateOwners(browser);var runtime=Owner<JavaScriptRuntime>(browser,"runtime");var dom=Owner<DomBridge>(browser,"dom");
        oldScope=((IWeakCollectionOwner)(object)runtime.Engine).WeakCollections;throwOnRelease=true;
        if(reason=="Release")browser.Release();
        else if(reason=="NavigationFailed")Assert.Equal("Navigation failed",browser.Navigate("about:Unsupported").Title);
        else Assert.Contains("JavaScript ready: 12",browser.Navigate("about:Start").Text,StringComparison.Ordinal);
        Assert.Equal(1,observed);CheckReleased(oldScope);Assert.Equal(0,dom.Lifetime.OutstandingHandles);Assert.Equal(0,runtime.PendingTaskCount);
        Assert.NotNull(observedPacket);Assert.Contains("|Reason|"+reason+"\n",observedPacket,StringComparison.Ordinal);
        Assert.Equal(oldScope.Lifetime,observedScope);Assert.True(noOwnersAtCallback);
        throwOnRelease=false;Assert.Contains("JavaScript ready: 12",browser.Navigate("about:Start").Text,StringComparison.Ordinal);browser.Release();
    }

    private sealed class TwoScriptsTransport:IWebTransport
    {
        public WebResponse Get(Uri address,BrowserLimits limits)=>new(address,200,"text/html","<html><body><script>1</script><script>2</script></body></html>");
    }
}