From e5435ecbd101f94065cf83f29d1f8a2ecccf87c9 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Thu, 4 Dec 2025 12:29:19 +0100 Subject: [PATCH 1/4] [Foundation] Fix nullability in NSMutableDictionary. * Enable nullability for NSMutableDictionary.cs. * Fix interface implementations to match nullability contracts (IDictionary, IDictionary). * Make indexers' return type allow null, but disallow setting null values. * Remove all 'To be added.' XML comments. * Add comprehensive XML documentation for all public methods, properties, and indexers. * Use proper 'see cref' attributes for type references in documentation. * Fix ArgumentNullException to use nameof for parameter names. * Remove an unused constructor. Also add NSMutableDictionary tests for missing key behavior: * Add tests to verify that accessing missing keys returns null. * Test all indexer overloads (string, NSObject, NSString). * Test ObjectForKey method with missing keys. * Test TryGetValue with missing keys returns false and null output. * Test IDictionary interface implementations (indexer and Contains). * Ensure existing keys still work correctly after testing missing keys. These tests document and verify the expected behavior when fetching values for keys that don't exist in the dictionary. This is file 34 of 47 files with nullability disabled in Foundation. Contributes towards https://github.com/dotnet/macios/issues/17285. --- src/AudioToolbox/AudioFile.cs | 5 +- src/AudioToolbox/AudioToolbox.cs | 4 +- src/CoreText/CTFont.cs | 36 ++- src/CoreText/CTFontDescriptor.cs | 4 +- src/CoreText/CTTextTab.cs | 4 +- src/Foundation/NSDictionary.cs | 42 ++-- src/Foundation/NSMutableDictionary.cs | 222 ++++++++++-------- src/Foundation/NSUrlSessionHandler.cs | 5 +- src/SceneKit/SCNParticleSystem.cs | 2 + src/Security/Items.cs | 7 +- src/WebKit/WebNavigationPolicyEventArgs.cs | 4 +- .../Documentation.KnownFailures.txt | 4 - .../Foundation/NSMutableDictionaryTest.cs | 112 +++++++++ 13 files changed, 298 insertions(+), 153 deletions(-) diff --git a/src/AudioToolbox/AudioFile.cs b/src/AudioToolbox/AudioFile.cs index 1abc4dfd5909..db9c72ccaea8 100644 --- a/src/AudioToolbox/AudioFile.cs +++ b/src/AudioToolbox/AudioFile.cs @@ -2376,10 +2376,11 @@ public unsafe byte []? ID3Tag { public AudioFileInfoDictionary? InfoDictionary { get { var ptr = GetIntPtr (AudioFileProperty.InfoDictionary); - if (ptr == IntPtr.Zero) + var dict = Runtime.GetNSObject (ptr, owns: true); + if (dict is null) return null; - return new AudioFileInfoDictionary (new NSMutableDictionary (ptr, true)); + return new AudioFileInfoDictionary (dict); } } diff --git a/src/AudioToolbox/AudioToolbox.cs b/src/AudioToolbox/AudioToolbox.cs index 6aee49c0421d..2dd9149cd005 100644 --- a/src/AudioToolbox/AudioToolbox.cs +++ b/src/AudioToolbox/AudioToolbox.cs @@ -39,8 +39,8 @@ internal InstrumentInfo (NSDictionary d) /// To be added. /// To be added. /// To be added. - public string Name { - get { return Dictionary [NameKey].ToString (); } + public string? Name { + get { return Dictionary [NameKey]?.ToString (); } } /// To be added. diff --git a/src/CoreText/CTFont.cs b/src/CoreText/CTFont.cs index 08cda95ea821..a1cd00a3b6c5 100644 --- a/src/CoreText/CTFont.cs +++ b/src/CoreText/CTFont.cs @@ -429,7 +429,10 @@ public string? Name { /// To be added. public FontFeatureGroup FeatureGroup { get { - return (FontFeatureGroup) (int) (NSNumber) Dictionary [CTFontFeatureKey.Identifier]; + var number = (NSNumber?) Dictionary [CTFontFeatureKey.Identifier]; + if (number is null) + return default; + return (FontFeatureGroup) (int) number; } } @@ -591,7 +594,10 @@ internal static CTFontFeatureSelectors Create (FontFeatureGroup featureGroup, NS /// To be added. protected int FeatureWeak { get { - return (int) (NSNumber) Dictionary [CTFontFeatureSelectorKey.Identifier]; + var number = (NSNumber?) Dictionary [CTFontFeatureSelectorKey.Identifier]; + if (number is null) + return default; + return (int) number; } } @@ -2327,7 +2333,10 @@ internal CTFontFeatureSettings (NSDictionary dictionary) /// To be added. public FontFeatureGroup FeatureGroup { get { - return (FontFeatureGroup) (int) (NSNumber) Dictionary [CTFontFeatureKey.Identifier]; + var number = (NSNumber?) Dictionary [CTFontFeatureKey.Identifier]; + if (number is null) + return default; + return (FontFeatureGroup) (int) number; } } @@ -2336,7 +2345,10 @@ public FontFeatureGroup FeatureGroup { /// To be added. public int FeatureWeak { get { - return (int) (NSNumber) Dictionary [CTFontFeatureSelectorKey.Identifier]; + var number = (NSNumber?) Dictionary [CTFontFeatureSelectorKey.Identifier]; + if (number is null) + return default; + return (int) number; } } } @@ -2375,32 +2387,32 @@ public CTFontVariationAxes (NSDictionary dictionary) /// To be added. /// To be added. /// To be added. - public NSNumber Identifier { - get { return (NSNumber) Dictionary [CTFontVariationAxisKey.Identifier]; } + public NSNumber? Identifier { + get { return (NSNumber?) Dictionary [CTFontVariationAxisKey.Identifier]; } set { Adapter.SetValue (Dictionary, CTFontVariationAxisKey.Identifier, value); } } /// To be added. /// To be added. /// To be added. - public NSNumber MinimumValue { - get { return (NSNumber) Dictionary [CTFontVariationAxisKey.MinimumValue]; } + public NSNumber? MinimumValue { + get { return (NSNumber?) Dictionary [CTFontVariationAxisKey.MinimumValue]; } set { Adapter.SetValue (Dictionary, CTFontVariationAxisKey.MinimumValue, value); } } /// To be added. /// To be added. /// To be added. - public NSNumber MaximumValue { - get { return (NSNumber) Dictionary [CTFontVariationAxisKey.MaximumValue]; } + public NSNumber? MaximumValue { + get { return (NSNumber?) Dictionary [CTFontVariationAxisKey.MaximumValue]; } set { Adapter.SetValue (Dictionary, CTFontVariationAxisKey.MaximumValue, value); } } /// To be added. /// To be added. /// To be added. - public NSNumber DefaultValue { - get { return (NSNumber) Dictionary [CTFontVariationAxisKey.DefaultValue]; } + public NSNumber? DefaultValue { + get { return (NSNumber?) Dictionary [CTFontVariationAxisKey.DefaultValue]; } set { Adapter.SetValue (Dictionary, CTFontVariationAxisKey.DefaultValue, value); } } diff --git a/src/CoreText/CTFontDescriptor.cs b/src/CoreText/CTFontDescriptor.cs index bd5a1be38766..64bfae986123 100644 --- a/src/CoreText/CTFontDescriptor.cs +++ b/src/CoreText/CTFontDescriptor.cs @@ -315,7 +315,7 @@ public IEnumerable? CascadeList { /// /// public NSCharacterSet? CharacterSet { - get { return (NSCharacterSet) Dictionary [CTFontDescriptorAttributeKey.CharacterSet]; } + get { return (NSCharacterSet) Dictionary [CTFontDescriptorAttributeKey.CharacterSet]!; } set { Adapter.SetValue (Dictionary, CTFontDescriptorAttributeKey.CharacterSet!, value); } } @@ -515,7 +515,7 @@ public bool? WeakEnabled { /// public bool Enabled { get { - var value = (NSNumber) Dictionary [CTFontDescriptorAttributeKey.Enabled]; + var value = (NSNumber?) Dictionary [CTFontDescriptorAttributeKey.Enabled]; if (value is null) return false; return value.Int32Value != 0; diff --git a/src/CoreText/CTTextTab.cs b/src/CoreText/CTTextTab.cs index 0f7199982583..fcb1a43b2bd6 100644 --- a/src/CoreText/CTTextTab.cs +++ b/src/CoreText/CTTextTab.cs @@ -63,8 +63,8 @@ public CTTextTabOptions (NSDictionary dictionary) /// To be added. /// To be added. /// To be added. - public NSCharacterSet ColumnTerminators { - get { return (NSCharacterSet) Dictionary [CTTextTabOptionKey.ColumnTerminators]; } + public NSCharacterSet? ColumnTerminators { + get { return (NSCharacterSet?) Dictionary [CTTextTabOptionKey.ColumnTerminators]!; } set { Adapter.SetValue (Dictionary, CTTextTabOptionKey.ColumnTerminators, value); } } } diff --git a/src/Foundation/NSDictionary.cs b/src/Foundation/NSDictionary.cs index 166ca912ebc9..d14e4261f4e2 100644 --- a/src/Foundation/NSDictionary.cs +++ b/src/Foundation/NSDictionary.cs @@ -23,6 +23,7 @@ // using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; // Disable until we get around to enable + fix any issues. #nullable disable @@ -472,13 +473,13 @@ public bool TryGetValue (NSObject key, out NSObject value) return value is not null; } - /// Key to lookup - /// Returns the value associated from a key in the dictionary, or null if the key is not found. - /// - /// - /// - /// - public virtual NSObject this [NSObject key] { +#nullable enable + /// Gets the object associated with the specified key. + /// The key of the object to get. + /// The object associated with the specified key. + /// Returns if the key wasn't found. + [DisallowNull] // don't allow setting null values + public virtual NSObject? this [NSObject key] { get { return ObjectForKey (key); } @@ -487,13 +488,12 @@ public virtual NSObject this [NSObject key] { } } - /// Key to lookup - /// Returns the value associated from a key in the dictionary, or null if the key is not found. - /// - /// - /// - /// - public virtual NSObject this [NSString key] { + /// Gets the object associated with the specified key. + /// The key of the object to get. + /// The object associated with the specified key. + /// Returns if the key wasn't found. + [DisallowNull] // don't allow setting null values + public virtual NSObject? this [NSString key] { get { return ObjectForKey (key); } @@ -502,12 +502,13 @@ public virtual NSObject this [NSString key] { } } - /// Key to lookup - /// Returns the value associated from a key in the dictionary, or null if the key is not found. - /// - /// - /// The string will be marshalled as an NSString before performing the lookup. - public virtual NSObject this [string key] { + /// Gets the object associated with the specified key. + /// The key of the object to get. + /// The object associated with the specified key. + /// Thrown when is . + /// Returns if the key wasn't found. + [DisallowNull] // don't allow setting null valuese performing the lookup. + public virtual NSObject? this [string key] { get { if (key is null) throw new ArgumentNullException ("key"); @@ -522,6 +523,7 @@ public virtual NSObject this [string key] { throw new NotSupportedException (); } } +#nullable disable ICollection IDictionary.Keys { get { return Keys; } diff --git a/src/Foundation/NSMutableDictionary.cs b/src/Foundation/NSMutableDictionary.cs index a6cebeb5b520..10a572a9896a 100644 --- a/src/Foundation/NSMutableDictionary.cs +++ b/src/Foundation/NSMutableDictionary.cs @@ -20,32 +20,25 @@ // // Copyright 2011 - 2014 Xamarin Inc (http://www.xamarin.com) // +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using CoreFoundation; -// Disable until we get around to enable + fix any issues. -#nullable disable +#nullable enable namespace Foundation { public partial class NSMutableDictionary : NSDictionary, IDictionary, IDictionary { - - // some API, like SecItemCopyMatching, returns a retained NSMutableDictionary - internal NSMutableDictionary (NativeHandle handle, bool owns) - : base (handle) - { - if (!owns) - DangerousRelease (); - } - - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Creates a mutable dictionary from the specified arrays of objects and keys. + /// The array of objects to add to the dictionary. + /// The array of keys for the objects. + /// A new mutable dictionary containing the specified objects and keys. + /// Thrown when or is . + /// Thrown when the arrays have different sizes. public static NSMutableDictionary FromObjectsAndKeys (NSObject [] objects, NSObject [] keys) { if (objects is null) @@ -60,11 +53,12 @@ public static NSMutableDictionary FromObjectsAndKeys (NSObject [] objects, NSObj return FromObjectsAndKeysInternal (no, nk); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Creates a mutable dictionary from the specified arrays of objects and keys. + /// The array of objects to add to the dictionary. + /// The array of keys for the objects. + /// A new mutable dictionary containing the specified objects and keys. + /// Thrown when or is . + /// Thrown when the arrays have different sizes. public static NSMutableDictionary FromObjectsAndKeys (object [] objects, object [] keys) { if (objects is null) @@ -79,12 +73,13 @@ public static NSMutableDictionary FromObjectsAndKeys (object [] objects, object return FromObjectsAndKeysInternal (no, nk); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Creates a mutable dictionary from the specified number of objects and keys from the arrays. + /// The array of objects to add to the dictionary. + /// The array of keys for the objects. + /// The number of elements to copy from the arrays. + /// A new mutable dictionary containing the specified objects and keys. + /// Thrown when or is . + /// Thrown when the arrays have different sizes or is invalid. public static NSMutableDictionary FromObjectsAndKeys (NSObject [] objects, NSObject [] keys, nint count) { if (objects is null) @@ -101,12 +96,13 @@ public static NSMutableDictionary FromObjectsAndKeys (NSObject [] objects, NSObj return FromObjectsAndKeysInternal (no, nk); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Creates a mutable dictionary from the specified number of objects and keys from the arrays. + /// The array of objects to add to the dictionary. + /// The array of keys for the objects. + /// The number of elements to copy from the arrays. + /// A new mutable dictionary containing the specified objects and keys. + /// Thrown when or is . + /// Thrown when the arrays have different sizes or is invalid. public static NSMutableDictionary FromObjectsAndKeys (object [] objects, object [] keys, nint count) { if (objects is null) @@ -129,8 +125,7 @@ void ICollection>.Add (KeyValuePairTo be added. - /// To be added. + /// Removes all objects from the dictionary. public void Clear () { RemoveAllObjects (); @@ -175,11 +170,11 @@ bool ICollection>.IsReadOnly { #endregion #region IDictionary - /// To be added. - /// To be added. - /// To be added. - /// To be added. - void IDictionary.Add (object key, object value) + /// Adds an element with the provided key and value to the dictionary. + /// The key of the element to add. + /// The value of the element to add. + /// Thrown when the key or value is not an . + void IDictionary.Add (object key, object? value) { var nsokey = key as NSObject; var nsovalue = value as NSObject; @@ -191,10 +186,10 @@ void IDictionary.Add (object key, object value) SetObject (nsovalue, nsokey); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Determines whether the dictionary contains an element with the specified key. + /// The key to locate in the dictionary. + /// if the dictionary contains an element with the key; otherwise, . + /// Thrown when is . bool IDictionary.Contains (object key) { if (key is null) @@ -207,17 +202,17 @@ bool IDictionary.Contains (object key) return result; } - /// To be added. - /// To be added. - /// To be added. + /// Returns an enumerator that iterates through the dictionary. + /// An for the dictionary. IDictionaryEnumerator IDictionary.GetEnumerator () { return (IDictionaryEnumerator) ((IEnumerable>) this).GetEnumerator (); } - /// To be added. - /// To be added. - /// To be added. + /// Removes the element with the specified key from the dictionary. + /// The key of the element to remove. + /// Thrown when is . + /// Thrown when the key is not an . void IDictionary.Remove (object key) { if (key is null) @@ -230,26 +225,28 @@ void IDictionary.Remove (object key) GC.KeepAlive (nskey); } - /// To be added. - /// To be added. - /// To be added. + /// Gets a value indicating whether the dictionary has a fixed size. + /// since the dictionary is mutable. bool IDictionary.IsFixedSize { get { return false; } } - /// To be added. - /// To be added. - /// To be added. + /// Gets a value indicating whether the dictionary is read-only. + /// since the dictionary is mutable. bool IDictionary.IsReadOnly { get { return false; } } - object IDictionary.this [object key] { + /// Gets or sets the object associated with the specified key. + /// The key of the object to get or set. + /// The object associated with the specified key. + /// Thrown when setting a value and the key or value is not an . + object? IDictionary.this [object key] { get { var _key = key as INativeObject; if (_key is null) return null; - object result = _ObjectForKey (_key.Handle); + object? result = _ObjectForKey (_key.Handle); GC.KeepAlive (_key); return result; } @@ -266,36 +263,33 @@ object IDictionary.this [object key] { } } - /// To be added. - /// To be added. - /// To be added. + /// Gets an containing the keys of the dictionary. + /// An containing the keys of the dictionary. ICollection IDictionary.Keys { get { return Keys; } } - /// To be added. - /// To be added. - /// To be added. + /// Gets an containing the values in the dictionary. + /// An containing the values in the dictionary. ICollection IDictionary.Values { get { return Values; } } #endregion #region IDictionary - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Adds an element with the provided key and value to the dictionary. + /// The key of the element to add. + /// The value of the element to add. public void Add (NSObject key, NSObject value) { // Inverted args. SetObject (value, key); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Removes the element with the specified key from the dictionary. + /// The key of the element to remove. + /// if the element is successfully removed; otherwise, . + /// Thrown when is . public bool Remove (NSObject key) { if (key is null) @@ -306,19 +300,27 @@ public bool Remove (NSObject key) return last != Count; } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public bool TryGetValue (NSObject key, out NSObject value) + /// Gets the value associated with the specified key. + /// The key whose value to get. + /// When this method returns, the value associated with the specified key, if the key is found; otherwise, . + /// if the dictionary contains an element with the specified key; otherwise, . +#pragma warning disable CS8767 // Nullability of reference types in type of parameter 'value' of 'bool NSMutableDictionary.TryGetValue(NSObject key, out NSObject? value)' doesn't match implicitly implemented member 'bool IDictionary.TryGetValue(NSObject key, out NSObject value)' (possibly because of nullability attributes). + public bool TryGetValue (NSObject key, [NotNullWhen (true)] out NSObject? value) +#pragma warning restore CS8767 { // Can't put null in NSDictionaries, so if null is returned, the key wasn't found. return (value = ObjectForKey (key)) is not null; } - public override NSObject this [NSObject key] { + /// Gets or sets the object associated with the specified key. + /// The key of the object to get or set. + /// The object associated with the specified key. + /// Returns if the key wasn't found. + [DisallowNull] // don't allow setting null values + public override NSObject? this [NSObject key] { +#pragma warning disable CS8766 // Nullability of reference types in return type of 'NSObject? NSMutableDictionary.this[NSObject key].get' doesn't match implicitly implemented member 'NSObject IDictionary.this[NSObject key].get' get { +#pragma warning restore CS8766 return ObjectForKey (key); } set { @@ -326,7 +328,12 @@ public override NSObject this [NSObject key] { } } - public override NSObject this [NSString key] { + /// Gets or sets the object associated with the specified key. + /// The key of the object to get or set. + /// The object associated with the specified key. + /// Returns if the key wasn't found. + [DisallowNull] // don't allow setting null values + public override NSObject? this [NSString key] { get { return ObjectForKey (key); } @@ -335,10 +342,16 @@ public override NSObject this [NSString key] { } } - public override NSObject this [string key] { + /// Gets or sets the object associated with the specified key. + /// The key of the object to get or set. + /// The object associated with the specified key. + /// Thrown when is . + /// Returns if the key wasn't found. + [DisallowNull] // don't allow setting null values + public override NSObject? this [string key] { get { if (key is null) - throw new ArgumentNullException ("key"); + throw new ArgumentNullException (nameof (key)); var nss = NSString.CreateNative (key, false); try { return Runtime.GetNSObject (LowlevelObjectForKey (nss)); @@ -348,7 +361,7 @@ public override NSObject this [string key] { } set { if (key is null) - throw new ArgumentNullException ("key"); + throw new ArgumentNullException (nameof (key)); var nss = NSString.CreateNative (key, false); try { LowlevelSetObject (value, nss); @@ -358,19 +371,22 @@ public override NSObject this [string key] { } } + /// Gets an containing the keys of the dictionary. + /// An containing the keys of the dictionary. ICollection IDictionary.Keys { get { return Keys; } } + /// Gets an containing the values in the dictionary. + /// An containing the values in the dictionary. ICollection IDictionary.Values { get { return Values; } } #endregion #region IEnumerable - /// To be added. - /// To be added. - /// To be added. + /// Returns an enumerator that iterates through the dictionary. + /// An for the dictionary. IEnumerator IEnumerable.GetEnumerator () { return ((IEnumerable>) this).GetEnumerator (); @@ -383,29 +399,27 @@ IEnumerator IEnumerable.GetEnumerator () public IEnumerator> GetEnumerator () { foreach (var key in Keys) { - yield return new KeyValuePair (key, ObjectForKey (key)); + yield return new KeyValuePair (key, this [key]!); } } #endregion - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Creates a mutable dictionary from a low-level object and key pointer. + /// The object pointer. + /// The key pointer. + /// A new mutable dictionary containing the specified object and key. public static NSMutableDictionary LowlevelFromObjectAndKey (IntPtr obj, IntPtr key) { #if MONOMAC - return (NSMutableDictionary) Runtime.GetNSObject (ObjCRuntime.Messaging.IntPtr_objc_msgSend_IntPtr_IntPtr (class_ptr, selDictionaryWithObject_ForKey_XHandle, obj, key)); + return (NSMutableDictionary) Runtime.GetNSObject (ObjCRuntime.Messaging.IntPtr_objc_msgSend_IntPtr_IntPtr (class_ptr, selDictionaryWithObject_ForKey_XHandle, obj, key))!; #else - return (NSMutableDictionary) Runtime.GetNSObject (ObjCRuntime.Messaging.IntPtr_objc_msgSend_IntPtr_IntPtr (class_ptr, Selector.GetHandle ("dictionaryWithObject:forKey:"), obj, key)); + return (NSMutableDictionary) Runtime.GetNSObject (ObjCRuntime.Messaging.IntPtr_objc_msgSend_IntPtr_IntPtr (class_ptr, Selector.GetHandle ("dictionaryWithObject:forKey:"), obj, key))!; #endif } - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Sets an object for a key using low-level pointers. + /// The object pointer. + /// The key pointer. public void LowlevelSetObject (IntPtr obj, IntPtr key) { #if MONOMAC @@ -415,10 +429,10 @@ public void LowlevelSetObject (IntPtr obj, IntPtr key) #endif } - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Sets an object for a key using a low-level key pointer. + /// The object to set. + /// The key pointer. + /// Thrown when is . public void LowlevelSetObject (NSObject obj, IntPtr key) { if (obj is null) @@ -428,6 +442,10 @@ public void LowlevelSetObject (NSObject obj, IntPtr key) GC.KeepAlive (obj); } + /// Sets a string value for a key using a low-level key pointer. + /// The string to set. + /// The key pointer. + /// Thrown when is . public void LowlevelSetObject (string str, IntPtr key) { if (str is null) diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index c12d535b6ce5..49a4aee1b1a3 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -93,10 +93,9 @@ public static string GetHeaderValue (this NSHttpCookie cookie) AppendSegment (header, NSHttpCookie.KeyExpires.ToString (), dateStr); } - if (cookie.Properties.ContainsKey (NSHttpCookie.KeyMaximumAge)) { - var timeStampString = (NSString) cookie.Properties [NSHttpCookie.KeyMaximumAge]; + var timeStampStringValue = cookie.Properties [NSHttpCookie.KeyMaximumAge]; + if (timeStampStringValue is NSString timeStampString) AppendSegment (header, NSHttpCookie.KeyMaximumAge.ToString (), timeStampString); - } if (cookie.IsSecure) AppendSegment (header, NSHttpCookie.KeySecure.ToString (), null); diff --git a/src/SceneKit/SCNParticleSystem.cs b/src/SceneKit/SCNParticleSystem.cs index 89beb77b049d..3418b86427d8 100644 --- a/src/SceneKit/SCNParticleSystem.cs +++ b/src/SceneKit/SCNParticleSystem.cs @@ -40,6 +40,8 @@ public SCNPropertyControllers () internal void Set (NSString key, SCNParticlePropertyController? value) { + ArgumentNullException.ThrowIfNull (value); + if (mutDict is null) { mutDict = new NSMutableDictionary (dict); dict = mutDict; diff --git a/src/Security/Items.cs b/src/Security/Items.cs index 05c91c93f439..402865f8d842 100644 --- a/src/Security/Items.cs +++ b/src/Security/Items.cs @@ -325,8 +325,11 @@ internal SecKeyChain (NativeHandle handle) copy.LowlevelSetObject (CFBoolean.TrueHandle, SecItem.ReturnAttributes); copy.LowlevelSetObject (CFBoolean.TrueHandle, SecItem.ReturnData); result = SecItem.SecItemCopyMatching (copy.Handle, out var ptr); - if (result == SecStatusCode.Success) - return new SecRecord (new NSMutableDictionary (ptr, true)); + if (result == SecStatusCode.Success) { + var dict = Runtime.GetNSObject (ptr, true); + if (dict is not null) + return new SecRecord (dict); + } return null; } } diff --git a/src/WebKit/WebNavigationPolicyEventArgs.cs b/src/WebKit/WebNavigationPolicyEventArgs.cs index 24d4f6bb114c..6fdedb28efd7 100644 --- a/src/WebKit/WebNavigationPolicyEventArgs.cs +++ b/src/WebKit/WebNavigationPolicyEventArgs.cs @@ -36,7 +36,7 @@ partial class WebNavigationPolicyEventArgs { /// To be added. /// To be added. public WebNavigationType NavigationType { - get { return (WebNavigationType) ((NSNumber) ActionInformation [WebPolicyDelegate.WebActionNavigationTypeKey]).Int32Value; } + get { return (WebNavigationType) (((NSNumber?) ActionInformation [WebPolicyDelegate.WebActionNavigationTypeKey])?.Int32Value ?? 0); } } /// To be added. @@ -64,7 +64,7 @@ public WebActionMouseButton MouseButton { /// To be added. /// To be added. public uint Flags { - get { return ((NSNumber) ActionInformation [WebPolicyDelegate.WebActionModifierFlagsKey]).UInt32Value; } + get { return ((NSNumber?) ActionInformation [WebPolicyDelegate.WebActionModifierFlagsKey])?.UInt32Value ?? 0; } } /// To be added. diff --git a/tests/cecil-tests/Documentation.KnownFailures.txt b/tests/cecil-tests/Documentation.KnownFailures.txt index b79794d94f89..cadf01b3917d 100644 --- a/tests/cecil-tests/Documentation.KnownFailures.txt +++ b/tests/cecil-tests/Documentation.KnownFailures.txt @@ -11851,7 +11851,6 @@ M:Foundation.NSKeyValueSharedObserverRegistration_NSObject.SetSharedObservers(Fo M:Foundation.NSKeyValueSharedObservers.#ctor(System.Type) M:Foundation.NSMachPort.Dispose(System.Boolean) M:Foundation.NSMetadataQuery.Dispose(System.Boolean) -M:Foundation.NSMutableDictionary.LowlevelSetObject(System.String,System.IntPtr) M:Foundation.NSMutableDictionary`2.FromObjectsAndKeys(`1[],`0[]) M:Foundation.NSMutableOrderedSet`1.op_Addition(Foundation.NSMutableOrderedSet{`0},Foundation.NSMutableOrderedSet{`0}) M:Foundation.NSMutableOrderedSet`1.op_Addition(Foundation.NSMutableOrderedSet{`0},Foundation.NSOrderedSet{`0}) @@ -21038,9 +21037,6 @@ P:Foundation.NSMetadataItem.UbiquitousItemDownloadingStatus P:Foundation.NSMorphology.Unspecified P:Foundation.NSMutableArray`1.Item(System.UIntPtr) P:Foundation.NSMutableData.Item(System.IntPtr) -P:Foundation.NSMutableDictionary.Item(Foundation.NSObject) -P:Foundation.NSMutableDictionary.Item(Foundation.NSString) -P:Foundation.NSMutableDictionary.Item(System.String) P:Foundation.NSMutableDictionary`2.Item(`0) P:Foundation.NSMutableOrderedSet`1.Item(System.IntPtr) P:Foundation.NSNetDomainEventArgs.Domain diff --git a/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs b/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs index c319e8dd264f..4cbe5dda037d 100644 --- a/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs +++ b/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs @@ -90,5 +90,117 @@ public void AddEntries () } } } + + [Test] + public void MissingKey_StringIndexer () + { + using (var dict = new NSMutableDictionary ()) { + dict ["existingKey"] = (NSString) "value"; + + // Accessing a missing key should return null + var result = dict ["missingKey"]; + Assert.IsNull (result, "Missing key should return null"); + + // Verify the existing key still works + Assert.IsNotNull (dict ["existingKey"], "Existing key should return value"); + } + } + + [Test] + public void MissingKey_NSObjectIndexer () + { + using (var dict = new NSMutableDictionary ()) { + dict [(NSString) "existingKey"] = (NSString) "value"; + + // Accessing a missing key should return null + var result = dict [(NSString) "missingKey"]; + Assert.IsNull (result, "Missing key should return null"); + + // Verify the existing key still works + Assert.IsNotNull (dict [(NSString) "existingKey"], "Existing key should return value"); + } + } + + [Test] + public void MissingKey_NSStringIndexer () + { + using (var dict = new NSMutableDictionary ()) { + dict [(NSString) "existingKey"] = (NSString) "value"; + + // Accessing a missing key should return null + var result = dict [(NSString) "missingKey"]; + Assert.IsNull (result, "Missing key should return null"); + + // Verify the existing key still works + Assert.IsNotNull (dict [(NSString) "existingKey"], "Existing key should return value"); + } + } + + [Test] + public void MissingKey_ObjectForKey () + { + using (var dict = new NSMutableDictionary ()) { + dict [(NSString) "existingKey"] = (NSString) "value"; + + // ObjectForKey with missing key should return null + var result = dict.ObjectForKey ((NSString) "missingKey"); + Assert.IsNull (result, "ObjectForKey with missing key should return null"); + + // Verify the existing key still works + Assert.IsNotNull (dict.ObjectForKey ((NSString) "existingKey"), "ObjectForKey with existing key should return value"); + } + } + + [Test] + public void MissingKey_TryGetValue () + { + using (var dict = new NSMutableDictionary ()) { + dict [(NSString) "existingKey"] = (NSString) "value"; + + // TryGetValue with missing key should return false + var found = dict.TryGetValue ((NSString) "missingKey", out var result); + Assert.IsFalse (found, "TryGetValue should return false for missing key"); + Assert.IsNull (result, "Output value should be null for missing key"); + + // Verify the existing key works + found = dict.TryGetValue ((NSString) "existingKey", out result); + Assert.IsTrue (found, "TryGetValue should return true for existing key"); + Assert.IsNotNull (result, "Output value should not be null for existing key"); + Assert.AreEqual ("value", result.ToString (), "Output value should match"); + } + } + + [Test] + public void MissingKey_IDictionaryIndexer () + { + using (var dict = new NSMutableDictionary ()) { + System.Collections.IDictionary idict = dict; + idict [(NSString) "existingKey"] = (NSString) "value"; + + // Accessing a missing key through IDictionary indexer returns IntPtr.Zero (not null) + // This is different from the typed indexers which return null + var result = idict [(NSString) "missingKey"]; + // The IDictionary indexer calls _ObjectForKey which returns IntPtr.Zero boxed + Assert.AreEqual (IntPtr.Zero, result, "IDictionary indexer with missing key returns IntPtr.Zero"); + + // Verify the existing key still works + Assert.IsNotNull (idict [(NSString) "existingKey"], "IDictionary indexer with existing key should return value"); + } + } + + [Test] + public void MissingKey_IDictionaryContains () + { + using (var dict = new NSMutableDictionary ()) { + System.Collections.IDictionary idict = dict; + idict [(NSString) "existingKey"] = (NSString) "value"; + + // Contains should return false for missing key + Assert.IsFalse (idict.Contains ((NSString) "missingKey"), "Contains should return false for missing key"); + + // Contains should return true for existing key + Assert.IsTrue (idict.Contains ((NSString) "existingKey"), "Contains should return true for existing key"); + } + } } } From d883529f09c0ff526b5650f21414dd12d255fa87 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 15 Dec 2025 10:07:05 +0100 Subject: [PATCH 2/4] Update src/Foundation/NSDictionary.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Foundation/NSDictionary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Foundation/NSDictionary.cs b/src/Foundation/NSDictionary.cs index d14e4261f4e2..ab5b760424c7 100644 --- a/src/Foundation/NSDictionary.cs +++ b/src/Foundation/NSDictionary.cs @@ -507,7 +507,7 @@ public virtual NSObject? this [NSString key] { /// The object associated with the specified key. /// Thrown when is . /// Returns if the key wasn't found. - [DisallowNull] // don't allow setting null valuese performing the lookup. + [DisallowNull] // don't allow setting null values public virtual NSObject? this [string key] { get { if (key is null) From 49a84c058c9689d76f6381e8c5672c23a73bec65 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 15 Dec 2025 10:09:13 +0100 Subject: [PATCH 3/4] Remove on null value. --- src/SceneKit/SCNParticleSystem.cs | 9 +- .../SceneKit/SCNParticleSystemTest.cs | 537 ++++++++++++++++++ 2 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 tests/monotouch-test/SceneKit/SCNParticleSystemTest.cs diff --git a/src/SceneKit/SCNParticleSystem.cs b/src/SceneKit/SCNParticleSystem.cs index 3418b86427d8..c98a04c8df20 100644 --- a/src/SceneKit/SCNParticleSystem.cs +++ b/src/SceneKit/SCNParticleSystem.cs @@ -40,13 +40,18 @@ public SCNPropertyControllers () internal void Set (NSString key, SCNParticlePropertyController? value) { - ArgumentNullException.ThrowIfNull (value); + if (value is null && mutDict is null) + return; if (mutDict is null) { mutDict = new NSMutableDictionary (dict); dict = mutDict; } - mutDict [key] = value; + if (value is null) { + mutDict.Remove (key); + } else { + mutDict [key] = value; + } } /// To be added. diff --git a/tests/monotouch-test/SceneKit/SCNParticleSystemTest.cs b/tests/monotouch-test/SceneKit/SCNParticleSystemTest.cs new file mode 100644 index 000000000000..ea4427448469 --- /dev/null +++ b/tests/monotouch-test/SceneKit/SCNParticleSystemTest.cs @@ -0,0 +1,537 @@ +// +// Unit tests for SCNParticleSystem +// +// Authors: +// Rolf Bjarne Kvinge +// +// Copyright 2025 Microsoft Corp. +// + +using System; +using CoreAnimation; +using CoreGraphics; +using Foundation; +using SceneKit; +#if MONOMAC +using AppKit; +#else +using UIKit; +#endif +using Xamarin.Utils; + +namespace MonoTouchFixtures.SceneKit { + + [TestFixture] + [Preserve (AllMembers = true)] + public class SCNParticleSystemTest { + + [Test] + public void Create () + { + using (var ps = SCNParticleSystem.Create ()) { + Assert.IsNotNull (ps, "Create should return non-null"); + Assert.AreNotEqual (IntPtr.Zero, ps.Handle, "Handle should not be zero"); + } + } + + [Test] + public void CreateNamed_Null () + { + Assert.Throws (() => SCNParticleSystem.Create (null, null), "Create with null name should throw"); + } + + [Test] + public void CreateNamed_InvalidName () + { + var ps = SCNParticleSystem.Create ("nonexistent", null); + Assert.IsNull (ps, "Create with invalid name should return null"); + } + + [Test] + public void CreateNamed_NullDirectory () + { + var ps = SCNParticleSystem.Create ("test", null); + // Should not throw, just return null if not found + Assert.IsNull (ps, "Create with null directory should return null if not found"); + } + + [Test] + public void PropertyControllers_Get () + { + using (var ps = SCNParticleSystem.Create ()) { + var controllers = ps.PropertyControllers; + // PropertyControllers can be null initially + // If not null, verify it's a valid object + if (controllers is not null) { + Assert.IsNotNull (controllers, "PropertyControllers should be non-null or null"); + } + } + } + + [Test] + public void PropertyControllers_SetNull () + { + using (var ps = SCNParticleSystem.Create ()) { + ps.PropertyControllers = null; + Assert.IsNull (ps.PropertyControllers, "PropertyControllers should be null after setting to null"); + } + } + + [Test] + public void PropertyControllers_Set () + { + using (var ps = SCNParticleSystem.Create ()) { + var controllers = new SCNPropertyControllers (); + ps.PropertyControllers = controllers; + Assert.IsNotNull (ps.PropertyControllers, "PropertyControllers should be non-null after setting"); + } + } + + [Test] + public void PropertyControllers_EmptyConstructor () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNotNull (controllers, "Empty constructor should create valid object"); + } + + [Test] + public void PropertyControllers_Position () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Position, "Position should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Position = controller; + Assert.IsNotNull (controllers.Position, "Position should be non-null after setting"); + } + + controllers.Position = null; + Assert.IsNull (controllers.Position, "Position should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Angle () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Angle, "Angle should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Angle = controller; + Assert.IsNotNull (controllers.Angle, "Angle should be non-null after setting"); + } + + controllers.Angle = null; + Assert.IsNull (controllers.Angle, "Angle should be null after setting to null"); + } + + [Test] + public void PropertyControllers_RotationAxis () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.RotationAxis, "RotationAxis should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.RotationAxis = controller; + Assert.IsNotNull (controllers.RotationAxis, "RotationAxis should be non-null after setting"); + } + + controllers.RotationAxis = null; + Assert.IsNull (controllers.RotationAxis, "RotationAxis should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Velocity () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Velocity, "Velocity should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Velocity = controller; + Assert.IsNotNull (controllers.Velocity, "Velocity should be non-null after setting"); + } + + controllers.Velocity = null; + Assert.IsNull (controllers.Velocity, "Velocity should be null after setting to null"); + } + + [Test] + public void PropertyControllers_AngularVelocity () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.AngularVelocity, "AngularVelocity should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.AngularVelocity = controller; + Assert.IsNotNull (controllers.AngularVelocity, "AngularVelocity should be non-null after setting"); + } + + controllers.AngularVelocity = null; + Assert.IsNull (controllers.AngularVelocity, "AngularVelocity should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Life () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Life, "Life should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Life = controller; + Assert.IsNotNull (controllers.Life, "Life should be non-null after setting"); + } + + controllers.Life = null; + Assert.IsNull (controllers.Life, "Life should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Color () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Color, "Color should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Color = controller; + Assert.IsNotNull (controllers.Color, "Color should be non-null after setting"); + } + + controllers.Color = null; + Assert.IsNull (controllers.Color, "Color should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Opacity () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Opacity, "Opacity should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Opacity = controller; + Assert.IsNotNull (controllers.Opacity, "Opacity should be non-null after setting"); + } + + controllers.Opacity = null; + Assert.IsNull (controllers.Opacity, "Opacity should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Size () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Size, "Size should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Size = controller; + Assert.IsNotNull (controllers.Size, "Size should be non-null after setting"); + } + + controllers.Size = null; + Assert.IsNull (controllers.Size, "Size should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Frame () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Frame, "Frame should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Frame = controller; + Assert.IsNotNull (controllers.Frame, "Frame should be non-null after setting"); + } + + controllers.Frame = null; + Assert.IsNull (controllers.Frame, "Frame should be null after setting to null"); + } + + [Test] + public void PropertyControllers_FrameRate () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.FrameRate, "FrameRate should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.FrameRate = controller; + Assert.IsNotNull (controllers.FrameRate, "FrameRate should be non-null after setting"); + } + + controllers.FrameRate = null; + Assert.IsNull (controllers.FrameRate, "FrameRate should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Bounce () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Bounce, "Bounce should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Bounce = controller; + Assert.IsNotNull (controllers.Bounce, "Bounce should be non-null after setting"); + } + + controllers.Bounce = null; + Assert.IsNull (controllers.Bounce, "Bounce should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Charge () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Charge, "Charge should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Charge = controller; + Assert.IsNotNull (controllers.Charge, "Charge should be non-null after setting"); + } + + controllers.Charge = null; + Assert.IsNull (controllers.Charge, "Charge should be null after setting to null"); + } + + [Test] + public void PropertyControllers_Friction () + { + var controllers = new SCNPropertyControllers (); + Assert.IsNull (controllers.Friction, "Friction should be null initially"); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Friction = controller; + Assert.IsNotNull (controllers.Friction, "Friction should be non-null after setting"); + } + + controllers.Friction = null; + Assert.IsNull (controllers.Friction, "Friction should be null after setting to null"); + } + + [Test] + public void PropertyControllers_MultipleProperties () + { + var controllers = new SCNPropertyControllers (); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var posController = SCNParticlePropertyController.Create (animation)) + using (var colorController = SCNParticlePropertyController.Create (animation)) + using (var sizeController = SCNParticlePropertyController.Create (animation)) { + controllers.Position = posController; + controllers.Color = colorController; + controllers.Size = sizeController; + + Assert.IsNotNull (controllers.Position, "Position should be set"); + Assert.IsNotNull (controllers.Color, "Color should be set"); + Assert.IsNotNull (controllers.Size, "Size should be set"); + } + } + + [Test] + public void PropertyControllers_ReplaceProperty () + { + var controllers = new SCNPropertyControllers (); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller1 = SCNParticlePropertyController.Create (animation)) + using (var controller2 = SCNParticlePropertyController.Create (animation)) { + controllers.Position = controller1; + Assert.IsNotNull (controllers.Position, "Position should be set to first controller"); + + controllers.Position = controller2; + Assert.IsNotNull (controllers.Position, "Position should be set to second controller"); + + controllers.Position = null; + Assert.IsNull (controllers.Position, "Position should be null after clearing"); + } + } + + [Test] + public void AddAnimation () + { + using (var ps = SCNParticleSystem.Create ()) + using (var animation = CAAnimation.CreateAnimation ()) { + ps.AddAnimation (animation, (string) null); + ps.AddAnimation (animation, "key1"); + ps.RemoveAllAnimations (); + } + } + + [Test] + public void AddAnimation_NullAnimation () + { + using (var ps = SCNParticleSystem.Create ()) { + Assert.Throws (() => ps.AddAnimation ((CAAnimation) null, "key"), "AddAnimation with null should throw"); + } + } + + [Test] + public void GetAnimationKeys () + { + using (var ps = SCNParticleSystem.Create ()) { + var keys = ps.GetAnimationKeys (); + Assert.IsNotNull (keys, "GetAnimationKeys should return non-null"); + Assert.AreEqual (0, keys.Length, "Should have no animation keys initially"); + + using (var animation = CAAnimation.CreateAnimation ()) { + ps.AddAnimation (animation, "key1"); + keys = ps.GetAnimationKeys (); + Assert.AreEqual (1, keys.Length, "Should have one animation key"); + } + } + } + + [Test] + public void RemoveAllAnimations () + { + using (var ps = SCNParticleSystem.Create ()) + using (var animation = CAAnimation.CreateAnimation ()) { + ps.AddAnimation (animation, "key1"); + ps.AddAnimation (animation, "key2"); + var keys = ps.GetAnimationKeys (); + Assert.AreEqual (2, keys.Length, "Should have two animation keys"); + + ps.RemoveAllAnimations (); + keys = ps.GetAnimationKeys (); + Assert.AreEqual (0, keys.Length, "Should have no animation keys after removal"); + } + } + + [Test] + public void Copy () + { + using (var ps = SCNParticleSystem.Create ()) + using (var copy = ps.Copy (null)) { + Assert.IsNotNull (copy, "Copy should return non-null"); + Assert.AreNotEqual (ps.Handle, copy.Handle, "Copy should have different handle"); + } + } + + [Test] + public void NSCoding () + { + using (var ps = SCNParticleSystem.Create ()) { + // Test encoding/decoding + var data = NSKeyedArchiver.GetArchivedData (ps, true, out var error); + Assert.IsNotNull (data, "Encoding should produce data"); + Assert.IsNull (error, "Encoding should not produce error"); + + var decoded = NSKeyedUnarchiver.GetUnarchivedObject (typeof (SCNParticleSystem), data, out error); + Assert.IsNotNull (decoded, "Decoding should produce object"); + Assert.IsNull (error, "Decoding should not produce error"); + Assert.IsInstanceOf (decoded, "Decoded object should be SCNParticleSystem"); + } + } + + [Test] + public void AddModifier_NullProperties () + { + using (var ps = SCNParticleSystem.Create ()) { + Assert.Throws (() => ps.AddModifier (null, SCNParticleModifierStage.PreDynamics, (data, dataStride, start, end, deltaTime) => { }), "AddModifier with null properties should throw"); + } + } + + [Test] + public void AddModifier_NullHandler () + { + using (var ps = SCNParticleSystem.Create ()) { + var properties = new NSString [] { (NSString) "position" }; + Assert.Throws (() => ps.AddModifier (properties, SCNParticleModifierStage.PreDynamics, null), "AddModifier with null handler should throw"); + } + } + + [Test] + public void AddModifier_EmptyProperties () + { + using (var ps = SCNParticleSystem.Create ()) { + var properties = new NSString [0]; + // Should not throw with empty array + ps.AddModifier (properties, SCNParticleModifierStage.PreDynamics, (data, dataStride, start, end, deltaTime) => { }); + } + } + + [Test] + public void HandleEvent_NullProperties () + { + using (var ps = SCNParticleSystem.Create ()) { + Assert.Throws (() => ps.HandleEvent (SCNParticleEvent.Birth, null, (data, dataStride, indices, count) => { }), "HandleEvent with null properties should throw"); + } + } + + [Test] + public void HandleEvent_NullHandler () + { + using (var ps = SCNParticleSystem.Create ()) { + var properties = new NSString [] { (NSString) "position" }; + Assert.Throws (() => ps.HandleEvent (SCNParticleEvent.Birth, properties, null), "HandleEvent with null handler should throw"); + } + } + + [Test] + public void HandleEvent_EmptyProperties () + { + using (var ps = SCNParticleSystem.Create ()) { + var properties = new NSString [0]; + // Should not throw with empty array + ps.HandleEvent (SCNParticleEvent.Birth, properties, (data, dataStride, indices, count) => { }); + } + } + + [Test] + public void PropertyControllers_RoundTrip () + { + using (var ps = SCNParticleSystem.Create ()) { + var controllers = new SCNPropertyControllers (); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var posController = SCNParticlePropertyController.Create (animation)) { + controllers.Position = posController; + ps.PropertyControllers = controllers; + + var retrieved = ps.PropertyControllers; + Assert.IsNotNull (retrieved, "Retrieved PropertyControllers should not be null"); + Assert.IsNotNull (retrieved.Position, "Retrieved Position controller should not be null"); + } + } + } + + [Test] + public void PropertyControllers_ClearAllProperties () + { + var controllers = new SCNPropertyControllers (); + + using (var animation = CAAnimation.CreateAnimation ()) + using (var controller = SCNParticlePropertyController.Create (animation)) { + controllers.Position = controller; + controllers.Angle = controller; + controllers.Velocity = controller; + controllers.Color = controller; + + Assert.IsNotNull (controllers.Position, "Position should be set"); + Assert.IsNotNull (controllers.Angle, "Angle should be set"); + Assert.IsNotNull (controllers.Velocity, "Velocity should be set"); + Assert.IsNotNull (controllers.Color, "Color should be set"); + + controllers.Position = null; + controllers.Angle = null; + controllers.Velocity = null; + controllers.Color = null; + + Assert.IsNull (controllers.Position, "Position should be null"); + Assert.IsNull (controllers.Angle, "Angle should be null"); + Assert.IsNull (controllers.Velocity, "Velocity should be null"); + Assert.IsNull (controllers.Color, "Color should be null"); + } + } + } +} From b61b97b421b5aeb31870b3ce4c8a246b2a5217ba Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 15 Dec 2025 10:18:00 +0100 Subject: [PATCH 4/4] Be sure to resolve to the expected indexer overload in test. --- .../monotouch-test/Foundation/NSMutableDictionaryTest.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs b/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs index 4cbe5dda037d..af0c20d3d162 100644 --- a/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs +++ b/tests/monotouch-test/Foundation/NSMutableDictionaryTest.cs @@ -110,14 +110,16 @@ public void MissingKey_StringIndexer () public void MissingKey_NSObjectIndexer () { using (var dict = new NSMutableDictionary ()) { - dict [(NSString) "existingKey"] = (NSString) "value"; + var existingKey = NSDate.Now; + var missingKey = NSDate.DistantPast; + dict [existingKey] = NSDate.DistantFuture; // Accessing a missing key should return null - var result = dict [(NSString) "missingKey"]; + var result = dict [missingKey]; Assert.IsNull (result, "Missing key should return null"); // Verify the existing key still works - Assert.IsNotNull (dict [(NSString) "existingKey"], "Existing key should return value"); + Assert.IsNotNull (existingKey, "Existing key should return value"); } }