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<object> Key,WeakReference<object> Value) AddDeadCycle(OwnedWeakTable<object,object> 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<object> Table,WeakReference<object> Value) AddDeadTable(WeakCollectionScope scope,object key)
{var table=new OwnedWeakTable<object,object>(scope);var value=new object();table.Add(key,value);return(new(table),new(value));}
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference<object> AddOwnerCycle(WeakCollectionScope scope,object key)
{var table=new OwnedWeakTable<object,object>(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<object> weak)=>weak.TryGetTarget(out _);
[Fact]
public void WeakStoragePrunesDeadCyclesAndDeadTablesWithoutFinalizersOrStrongerRegistryRoots()
{
var scope=new WeakCollectionScope();var table=new OwnedWeakTable<object,object>(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<ObjectDisposedException>(()=>table.Add(key,new object()));
Assert.Throws<ObjectDisposedException>(()=>table.AddOrUpdate(key,new object()));
Assert.Throws<ObjectDisposedException>(()=>table.TryGetValue(key,out _));
Assert.Throws<ObjectDisposedException>(()=>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<IWeakCollectionOwner>((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);
}
}
}
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<object> Key,WeakReference<object> Value) AddDeadCycle(OwnedWeakTable<object,object> 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<object> Table,WeakReference<object> Value) AddDeadTable(WeakCollectionScope scope,object key)
{var table=new OwnedWeakTable<object,object>(scope);var value=new object();table.Add(key,value);return(new(table),new(value));}
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference<object> AddOwnerCycle(WeakCollectionScope scope,object key)
{var table=new OwnedWeakTable<object,object>(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<object> weak)=>weak.TryGetTarget(out _);
[Fact]
public void WeakStoragePrunesDeadCyclesAndDeadTablesWithoutFinalizersOrStrongerRegistryRoots()
{
var scope=new WeakCollectionScope();var table=new OwnedWeakTable<object,object>(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<ObjectDisposedException>(()=>table.Add(key,new object()));
Assert.Throws<ObjectDisposedException>(()=>table.AddOrUpdate(key,new object()));
Assert.Throws<ObjectDisposedException>(()=>table.TryGetValue(key,out _));
Assert.Throws<ObjectDisposedException>(()=>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<IWeakCollectionOwner>((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);
}
}
}