using System.Runtime.CompilerServices; using XFESystem.Collections; namespace XFESystem.Browser.Tests; [Collection("Browser lifetime ownership")] public sealed class WeakCollectionTests { [Fact] public void PublicNativeRegressionRunsRealIntrinsicsAndFreesAllOwnedSlots() { var result=BrowserWeakCollectionsRegression.Run(); Assert.True(result.Passed,result.Actual);Assert.Equal(result.Expected,result.Actual); Assert.Equal(result.AfterRelease.Created,result.AfterRelease.Disposed); } private sealed record BackReference(object Owner); [MethodImpl(MethodImplOptions.NoInlining)] private static (WeakReference Key,WeakReference Value) AddDeadCycle(OwnedWeakTable table) {var key=new object();var value=new BackReference(key);table.Add(key,value);return(new(key),new(value));} [MethodImpl(MethodImplOptions.NoInlining)] private static (WeakReference Table,WeakReference Value) AddDeadTable(WeakCollectionScope scope,object key) {var table=new OwnedWeakTable(scope);var value=new object();table.Add(key,value);return(new(table),new(value));} [MethodImpl(MethodImplOptions.NoInlining)] private static WeakReference AddOwnerCycle(WeakCollectionScope scope,object key) {var table=new OwnedWeakTable(scope);table.Add(key,new BackReference(table));return new(table);} private static void Collect(){GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();} [MethodImpl(MethodImplOptions.NoInlining)] private static bool HasTarget(WeakReference weak)=>weak.TryGetTarget(out _); [Fact] public void WeakStoragePrunesDeadCyclesAndDeadTablesWithoutFinalizersOrStrongerRegistryRoots() { var scope=new WeakCollectionScope();var table=new OwnedWeakTable(scope); var cycle=AddDeadCycle(table);Collect(); Assert.False(cycle.Key.TryGetTarget(out _));Assert.False(cycle.Value.TryGetTarget(out _)); var dead=scope.Prune();Assert.Equal(0,dead.Entries);Assert.Equal(dead.Created,dead.Disposed); var key=new object();var abandoned=AddDeadTable(scope,key);Collect(); Assert.False(HasTarget(abandoned.Table));Assert.True(HasTarget(abandoned.Value)); var pruned=scope.Prune();Assert.Equal(1,pruned.Tables);Assert.Equal(1,pruned.TableHandlesDisposed); Collect();Assert.False(HasTarget(abandoned.Value)); // Actual .NET CWT likewise keeps owner alive when a rooted key's value // refers back to that owner; the registry must not add another such edge. var owner=AddOwnerCycle(scope,key);Collect();Assert.True(owner.TryGetTarget(out _)); scope.Release();scope.Release();var final=scope.Lifetime; Assert.Equal(0,final.Tables);Assert.Equal(0,final.Entries);Assert.Equal(final.Created,final.Disposed); Assert.Equal(final.TableHandlesCreated,final.TableHandlesDisposed); Assert.Throws(()=>table.Add(key,new object())); Assert.Throws(()=>table.AddOrUpdate(key,new object())); Assert.Throws(()=>table.TryGetValue(key,out _)); Assert.Throws(()=>table.Remove(key)); Assert.Equal(final,scope.Lifetime); GC.KeepAlive(key);GC.KeepAlive(table); } [Fact] public void OriginalWeakIntrinsicsKeepValidationIdentityDescriptorsChainingAndComputedInsert() { var runtime=new JavaScriptRuntime(); Assert.NotNull(typeof(Jint.Engine).Assembly.GetType("XFESystem.JintOwnedWeakCollectionsV1")); Assert.IsAssignableFrom((object)runtime.Engine); var result=runtime.Evaluate(""" const k={},v={},m=new WeakMap([[k,v]]),s=new WeakSet([k]); let bad=0;try{new WeakMap()}catch(e){bad+=100}try{WeakMap()}catch(e){bad+=e instanceof TypeError?1:100} try{m.set(1,2)}catch(e){bad+=e instanceof TypeError?1:100}try{s.add(1)}catch(e){bad+=e instanceof TypeError?1:100} const same=m.set(k,v)===m&&m.get(k)===v&&m.has(k)&&s.add(k)===s&&s.has(k); const primitive=m.get(4)===undefined&&!m.has(4)&&!m.delete(4)&&!s.has(4)&&!s.delete(4); const desc=!Object.getOwnPropertyDescriptor(WeakMap.prototype,'set').enumerable&&Object.getPrototypeOf(m)===WeakMap.prototype; m.delete(k);const inserted=m.getOrInsert(k,21)===21&&m.getOrInsert(k,99)===21; m.delete(k);const computed=m.getOrInsertComputed(k,()=>{m.set(k,77);return 42})===42&&m.get(k)===42; [same,primitive,desc,inserted,computed,bad,s.delete(k),!s.has(k)].join(',') """); Assert.True(result.Success,result.Error);Assert.Equal("true,true,true,true,true,3,true,true",result.Text); runtime.Release();var life=((IWeakCollectionOwner)(object)runtime.Engine).WeakCollections.Lifetime; Assert.Equal(life.Created,life.Disposed);Assert.Equal(life.TableHandlesCreated,life.TableHandlesDisposed); } [Fact] public void RepeatedEnginePagesReleaseEveryNativeSlotAndPendingTimer() { for(int page=0;page<12;page++) { var runtime=new JavaScriptRuntime(); Assert.True(runtime.Evaluate("globalThis.map=new WeakMap();globalThis.set=new WeakSet();for(let i=0;i<100;i++){const k={};map.set(k,{key:k});set.add(k)}setInterval(()=>map.has({}),1);'ready'").Success); Collect();runtime.Pump(200);var owner=(IWeakCollectionOwner)(object)runtime.Engine;var pruned=owner.WeakCollections.Prune(); Assert.InRange(pruned.Entries,0,4);Assert.True(pruned.Disposed>=190); runtime.Release();var after=owner.WeakCollections.Lifetime; Assert.Equal(0,runtime.PendingTaskCount);Assert.Equal(0,after.Tables);Assert.Equal(after.Created,after.Disposed); Assert.Equal(after.TableHandlesCreated,after.TableHandlesDisposed); } } }