diff --git a/.agents/skills/macios-ci-failure-inspector/SKILL.md b/.agents/skills/macios-ci-failure-inspector/SKILL.md new file mode 100644 index 000000000000..d7efd06f8175 --- /dev/null +++ b/.agents/skills/macios-ci-failure-inspector/SKILL.md @@ -0,0 +1,231 @@ +--- +name: macios-ci-failure-inspector +description: Investigate and triage CI failures for dotnet/macios from Azure DevOps build URLs. Use this skill whenever the user shares a DevOps build link, asks about CI failures, wants to understand why a build failed, or asks to investigate test failures on any platform (iOS, tvOS, macOS, Mac Catalyst). Also use when the user says things like "CI is red", "tests are failing", "build broke", or "what happened in CI". +--- + +# macios CI Failure Inspector + +Investigate Azure DevOps CI failures for the dotnet/macios repository, extract root causes, and report findings. + +## References + +Read these as needed during investigation: + +- `references/azure-devops-cli.md` — az CLI commands, artifact naming conventions, and JSON parsing caveats. Read this when you need to construct `az` commands or download artifacts. + +## Inputs + +Collect from the user: + +- **Build URL** — an Azure DevOps build results link, e.g. `https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=&view=results` +- **Scope** — whether to investigate only, or also attempt fixes (default: investigate only) + +Extract the `buildId` from the URL query parameter. + +## Investigation workflow + +### Phase 1: Build overview + +Fetch the build metadata to understand the big picture: + +```bash +az pipelines build show --id --org https://devdiv.visualstudio.com --project DevDiv -o json > /tmp/build_show.json +``` + +Extract from the output: +- `result` (succeeded, failed, partiallySucceeded, canceled) +- `sourceBranch` — what branch triggered the build +- `definition.name` — which pipeline ran +- `triggerInfo` or `reason` — what triggered it (PR, push, schedule) + +If the build succeeded, tell the user and stop. + +### Phase 2: Timeline — identify failing jobs and tasks + +The timeline gives you every job and task in the build with its result: + +```bash +az devops invoke --area build --resource timeline --route-parameters project=DevDiv buildId= --org https://devdiv.visualstudio.com -o json > /tmp/build_timeline.json +``` + +Parse the timeline to find failed records. Use Python for robust JSON parsing because `az devops invoke` output can include trailing non-JSON text: + +```python +import json + +with open('/tmp/build_timeline.json', 'r') as f: + content = f.read() +data = json.JSONDecoder().raw_decode(content)[0] + +failed = [r for r in data.get('records', []) if r.get('result') == 'failed'] +for r in failed: + print(f" [{r['type']}] {r['name']} (id={r['id']}, logId={r.get('log', {}).get('id', 'N/A')})") +``` + +Group failures into categories: +- **Test failures** — tasks named "Run tests" or jobs like `T: monotouch_ios`, `T: monotouch_tvos`, `macOS tests` +- **Infrastructure failures** — tasks like "Provision Xcode", "Reserve bot", setup tasks +- **Build failures** — compilation or packaging tasks + +### Phase 3: Download TestSummary artifacts (primary failure source) + +The xharness test runner logs are 40K+ lines and don't contain standard NUnit failure patterns inline. **TestSummary artifacts are the fastest and most reliable way to identify failures.** Always start with these before digging into raw logs. + +List all artifacts: + +```bash +az pipelines runs artifact list --run-id --org https://devdiv.visualstudio.com --project DevDiv -o json +``` + +Download TestSummary artifacts for each failing job. **Each artifact must go to a separate directory** to avoid overwriting (they all contain a file named `TestSummary.md`): + +```bash +artifact="TestSummary-simulator_testsmonotouch_macos-1" +mkdir -p "/tmp/ci-artifacts/${artifact}" +az pipelines runs artifact download \ + --artifact-name "$artifact" \ + --path "/tmp/ci-artifacts/${artifact}" \ + --run-id \ + --org https://devdiv.visualstudio.com --project DevDiv +cat "/tmp/ci-artifacts/${artifact}/TestSummary.md" +``` + +The TestSummary.md file contains a structured markdown report with: +- Count of passed/failed tests +- For each failure: test configuration name, failure type (BuildFailure, Failed, Crashed, TimedOut), and brief error message +- Build failures show the configuration variant (e.g. "monotouch-test/macOS/Debug (ARM64): BuildFailure") +- Test failures may include the failing test class and assertion message + +Common artifact names map to timeline jobs: +- `TestSummary-simulator_testsmonotouch_ios-1` → monotouch_ios +- `TestSummary-simulator_testsmonotouch_tvos-1` → monotouch_tvos +- `TestSummary-simulator_testsmonotouch_macos-1` → monotouch_macos +- `TestSummary-simulator_testsmonotouch_maccatalyst-1` → monotouch_maccatalyst +- `TestSummary-simulator_testsdotnettests_ios-1` → dotnettests_ios +- `TestSummary-simulator_testsdotnettests_tvos-1` → dotnettests_tvos +- `TestSummary-simulator_testsdotnettests_macos-1` → dotnettests_macos +- `TestSummary-simulator_testsdotnettests_maccatalyst-1` → dotnettests_maccatalyst + +Download these in parallel for all failing jobs to save time. + +### Phase 4: Get detailed test failure info from HtmlReport artifacts + +For test failures (not build failures), download the corresponding HtmlReport artifact to get NUnit XML with exact test names, assertion messages, and stack traces: + +```bash +artifact="HtmlReport-simulator_testsmonotouch_tvos-1" +mkdir -p "/tmp/ci-artifacts/${artifact}" +az pipelines runs artifact download \ + --artifact-name "$artifact" \ + --path "/tmp/ci-artifacts/${artifact}" \ + --run-id \ + --org https://devdiv.visualstudio.com --project DevDiv +cd "/tmp/ci-artifacts/${artifact}" && unzip -o HtmlReport.zip -d htmlreport +``` + +Parse the NUnit XML files inside for specific test failures: + +```python +import xml.etree.ElementTree as ET +import glob + +xml_files = glob.glob('htmlreport/tests/monotouch-test/*/test-ios-*.xml') +for xf in sorted(xml_files): + tree = ET.parse(xf) + for tc in tree.getroot().iter('test-case'): + if tc.get('result') == 'Failed': + fullname = tc.get('fullname', 'unknown') + msg_el = tc.find('.//message') + msg = msg_el.text[:200] if msg_el is not None and msg_el.text else '' + trace_el = tc.find('.//stack-trace') + trace = trace_el.text[:300] if trace_el is not None and trace_el.text else '' + print(f"FAILED: {fullname}") + print(f" Message: {msg}") + if trace: + print(f" Stack: {trace}") +``` + +### Phase 5: Extract build error details from raw logs (for BuildFailure cases) + +Only use raw task logs when TestSummary shows BuildFailure and you need the specific compiler/build error. The logs are typically 40K+ lines — search narrowly: + +```bash +az devops invoke --area build --resource logs \ + --route-parameters project=DevDiv buildId= logId= \ + --org https://devdiv.visualstudio.com -o json > /tmp/build_log.json +``` + +Parse and search for build errors only: + +```python +import json + +with open('/tmp/build_log.json', 'r') as f: + data = json.JSONDecoder().raw_decode(f.read())[0] +lines = data.get('value', []) + +for i, line in enumerate(lines): + s = line.strip() + if ': error MSB' in s or ': error CS' in s or ': error NU' in s: + print(f"L{i}: {s[:300]}") + elif 'cannot execute tool' in s or 'MetalToolchain' in s: + print(f"L{i}: {s[:300]}") + elif 'Build FAILED' in s: + print(f"L{i}: {s[:300]}") +``` + +The xharness summary section near the end of the log also provides a task-level overview. Search backwards from the end for `Summary:`: + +```python +for i in range(len(lines)-1, -1, -1): + if 'Summary:' in lines[i]: + for j in range(i, min(len(lines), i+10)): + print(lines[j].strip()) + break +``` + +This shows `Executed N tasks`, `Succeeded: N`, `Failed: N`, `Crashed: N`, etc. + +### Phase 6: Categorize and report + +Group all findings by category and severity. Use this report structure: + +``` +## CI Failure Report — Build + +**Pipeline:** +**Branch:** +**Result:** + +### Failing Jobs + +#### (logId: ) +- **Category:** Test failure | Infrastructure | Build error +- **Failing tests:** + - `` — + - ... +- **Root cause:** + +### Infrastructure Issues +- + +### Summary +- Total failing jobs: N +- Test failures: N (list unique test names) +- Infrastructure failures: N +- Recommended actions: ... +``` + +## Troubleshooting the investigation + +### `az devops invoke` returns non-JSON output +The output can contain trailing text after the JSON object. Always use `json.JSONDecoder().raw_decode()` for parsing rather than `json.loads()`. + +### Timeline has many records +Filter by `result == 'failed'` first. If you need to understand job hierarchy, use the `parentId` field to trace task → job → stage relationships. + +### Artifact download fails +Some artifacts may only be available for a limited time or may require specific permissions. If download fails, fall back to log-based analysis. + +### Multiple build URLs provided +Investigate each build independently but cross-reference failures — if the same test fails across multiple builds, it's likely a real regression rather than flakiness. diff --git a/.agents/skills/macios-ci-failure-inspector/references/azure-devops-cli.md b/.agents/skills/macios-ci-failure-inspector/references/azure-devops-cli.md new file mode 100644 index 000000000000..8468d08b5598 --- /dev/null +++ b/.agents/skills/macios-ci-failure-inspector/references/azure-devops-cli.md @@ -0,0 +1,95 @@ +# Azure DevOps CLI Reference for macios CI + +## Authentication + +The `az devops` CLI must be authenticated. Typically this is done via: +```bash +az devops configure --defaults organization=https://devdiv.visualstudio.com project=DevDiv +``` + +Or by passing `--org` and `--project` on each command. + +## Key Commands + +### Build metadata +```bash +az pipelines build show --id -o json +``` +Returns: result, status, sourceBranch, definition, requestedFor, startTime, finishTime. + +### Build timeline (jobs and tasks) +```bash +az devops invoke --area build --resource timeline \ + --route-parameters project=DevDiv buildId= \ + --org https://devdiv.visualstudio.com -o json +``` +Returns: records array with type (Stage/Job/Task), name, result, state, log.id, parentId. + +**Important:** `az pipelines build log list` is NOT a valid command. Use the `az devops invoke` approach above. + +### Task logs +```bash +az devops invoke --area build --resource logs \ + --route-parameters project=DevDiv buildId= logId= \ + --org https://devdiv.visualstudio.com -o json +``` +Returns: value array of log line strings. + +### Artifact listing +```bash +az pipelines runs artifact list --run-id -o json +``` + +### Artifact download +```bash +az pipelines runs artifact download \ + --artifact-name "" \ + --path /tmp/ci-artifacts/ \ + --run-id +``` + +## Common Pipeline Names + +- `xamarin-macios-sim-pr-tests` — PR validation with simulator tests +- Other pipeline names may vary; check `definition.name` from build show. + +## Common Job Names in Timeline + +- `T: monotouch_ios` — iOS monotouch tests +- `T: monotouch_tvos` — tvOS monotouch tests +- `macOS tests` — macOS and Mac Catalyst tests +- `Reserve macOS bot for tests` — bot provisioning +- Various build/packaging jobs + +## JSON Parsing Caveat + +`az devops invoke` output may include trailing non-JSON text. Always parse with: +```python +import json +with open('file.json', 'r') as f: + content = f.read() +data = json.JSONDecoder().raw_decode(content)[0] +``` + +Do NOT use `json.loads(content)` directly — it will fail on the trailing text. + +## Test Artifact Names + +TestSummary and HtmlReport artifacts follow a naming convention: +- `TestSummary-simulator_tests-1` — Markdown summary with pass/fail counts and failure details +- `HtmlReport-simulator_tests-1` — ZIP containing HTML report and NUnit XML files + +Common job names: +- `monotouch_ios`, `monotouch_tvos`, `monotouch_macos`, `monotouch_maccatalyst` +- `dotnettests_ios`, `dotnettests_tvos`, `dotnettests_macos`, `dotnettests_maccatalyst` +- `cecil`, `framework`, `xtro`, `msbuild`, `generator`, `sharpie`, `fsharp`, `linker` +- `introspection`, `xcframework`, `interdependent_binding_projects` + +**Important:** Each artifact download overwrites `TestSummary.md` in the target directory. Always download to separate subdirectories named after the artifact. + +## Key Investigation Strategy + +1. **Start with TestSummary artifacts** — they are the fastest way to identify what failed and why. Raw task logs are 40K+ lines and don't contain standard NUnit patterns inline. +2. **For test failures (not build failures)**, download HtmlReport artifacts and parse the NUnit XML files inside for exact test names, assertions, and stack traces. +3. **Only use raw task logs** when you need build error details (MSB/CS/NU errors) or infrastructure error context. +4. **Map timeline logIds to jobs** using the `parentId` field to trace task → job relationships. diff --git a/dotnet/targets/Microsoft.Sdk.Desktop.targets b/dotnet/targets/Microsoft.Sdk.Desktop.targets index c963d184024f..6b878200c30a 100644 --- a/dotnet/targets/Microsoft.Sdk.Desktop.targets +++ b/dotnet/targets/Microsoft.Sdk.Desktop.targets @@ -15,7 +15,9 @@ <_OpenArguments Condition="'$(StandardInputPath)' != ''">$(_OpenArguments) --stdin "$(StandardInputPath)" <_OpenArgumentsPre Condition="'$(OpenNewInstance)' == 'true'">-n <_OpenArgumentsPre Condition="'$(OpenWaitForExit)' == 'true'">$(_OpenArgumentsPre) -W - <_OpenArguments>$(_OpenArguments) $(RunEnvironment) + <_OpenRunEnvironment>$(RunEnvironment) + <_OpenRunEnvironment>$(_OpenRunEnvironment) @(RuntimeEnvironmentVariable->'--env "%(Identity)=%(Value)"', ' ') + <_OpenArguments>$(_OpenArguments) $(_OpenRunEnvironment) open $(_OpenArgumentsPre) -a "$(TargetDir)/$(_AppBundleName).app" $(OpenArguments) $(_OpenArguments) --args diff --git a/dotnet/targets/Microsoft.Sdk.Mobile.targets b/dotnet/targets/Microsoft.Sdk.Mobile.targets index 0524dea71dfd..7ecf8141a073 100644 --- a/dotnet/targets/Microsoft.Sdk.Mobile.targets +++ b/dotnet/targets/Microsoft.Sdk.Mobile.targets @@ -92,6 +92,7 @@ + diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index 186244756bc7..4df059ea1d25 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -63,6 +63,7 @@ + diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs index 5bf54925b1ef..b9008d1a1d19 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/GetMlaunchArguments.cs @@ -311,8 +311,16 @@ protected string GenerateCommandLineCommands () sb.Add (StandardErrorPath); } - foreach (var envvar in EnvironmentVariables) - sb.Add ("--setenv=" + envvar.ItemSpec); + foreach (var envvar in EnvironmentVariables) { + var hasValue = envvar.MetadataNames.Cast ().Contains ("Value"); + if (hasValue) { + var value = envvar.GetMetadata ("Value"); + sb.Add ("--setenv=" + envvar.ItemSpec + "=" + value); + + } else { + sb.Add ("--setenv=" + envvar.ItemSpec); + } + } sb.Add (WaitForExit ? "--wait-for-exit:true" : "--wait-for-exit:false"); diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index a0cd06a1fa02..958b6216b73e 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -1308,6 +1308,9 @@ Copyright (C) 2018 Microsoft. All rights reserved. <_SceneKitCache>$(DeviceSpecificIntermediateOutputPath)copySceneKitAssets\_BundleResourceWithLogicalName.items + + <_SmeltedMetalCache>$(DeviceSpecificIntermediateOutputPath)metal\_SmeltedMetal.items + <_TextureAtlasCache>$(DeviceSpecificIntermediateOutputPath)atlas\_BundleResourceWithLogicalName.items @@ -1584,10 +1587,28 @@ Copyright (C) 2018 Microsoft. All rights reserved. _ComputeTargetFrameworkMoniker; _ReadAppManifest; _SetResourceMetadata; + _BeforeSmeltMetal; + _ReadSmeltedMetal; - + + + + + + + + + + + + + + + + + + %(Filename).dSYM <_PostProcessingItem Include="$([System.IO.Path]::GetFileName('$(AppBundleDir)'))/$(_NativeExecutableRelativePath)" Condition="'$(IsWatchApp)' != 'true'"> - $(_SymbolsListFullPath) + $(_SymbolsListFullPath) $(_AppBundleName)$(AppBundleExtension).dSYM $(IsXpcService) $(IsAppExtension) diff --git a/src/Accessibility/AXHearingUtilities.cs b/src/Accessibility/AXHearingUtilities.cs index e8f061a495ac..0bbb8fb63d45 100644 --- a/src/Accessibility/AXHearingUtilities.cs +++ b/src/Accessibility/AXHearingUtilities.cs @@ -38,7 +38,7 @@ public static AXHearingDeviceEar GetMFiHearingDeviceStreamingEar () public static NSUuid [] GetMFiHearingDevicePairedUuids () { - return NSArray.ArrayFromHandle (AXMFiHearingDevicePairedUUIDs ()); + return NSArray.NonNullArrayFromHandleDropNullElements (AXMFiHearingDevicePairedUUIDs ()); } } } diff --git a/src/AddressBook/ABAddressBook.cs b/src/AddressBook/ABAddressBook.cs index 5fde5ef1b33a..1cad31d1891e 100644 --- a/src/AddressBook/ABAddressBook.cs +++ b/src/AddressBook/ABAddressBook.cs @@ -422,7 +422,7 @@ public nint PeopleCount { public ABPerson [] GetPeople () { var cfArrayRef = ABAddressBookCopyArrayOfAllPeople (GetCheckedHandle ()); - return NSArray.ArrayFromHandle (cfArrayRef, h => new ABPerson (h, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, h => new ABPerson (h, this), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] @@ -438,7 +438,7 @@ public ABPerson [] GetPeople (ABRecord source) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (source)); var cfArrayRef = ABAddressBookCopyArrayOfAllPeopleInSource (GetCheckedHandle (), source.Handle); GC.KeepAlive (source); - return NSArray.ArrayFromHandle (cfArrayRef, l => new ABPerson (l, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, l => new ABPerson (l, this), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] @@ -455,7 +455,7 @@ public ABPerson [] GetPeople (ABRecord source, ABPersonSortBy sortOrdering) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (source)); var cfArrayRef = ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering (GetCheckedHandle (), source.Handle, sortOrdering); GC.KeepAlive (source); - return NSArray.ArrayFromHandle (cfArrayRef, l => new ABPerson (l, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, l => new ABPerson (l, this), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] @@ -490,7 +490,7 @@ public nint GroupCount { public ABGroup [] GetGroups () { var cfArrayRef = ABAddressBookCopyArrayOfAllGroups (GetCheckedHandle ()); - return NSArray.ArrayFromHandle (cfArrayRef, h => new ABGroup (h, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, h => new ABGroup (h, this), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] @@ -507,7 +507,7 @@ public ABGroup [] GetGroups (ABRecord source) var cfArrayRef = ABAddressBookCopyArrayOfAllGroupsInSource (GetCheckedHandle (), source.Handle); GC.KeepAlive (source); - return NSArray.ArrayFromHandle (cfArrayRef, l => new ABGroup (l, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, l => new ABGroup (l, this), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] @@ -720,7 +720,7 @@ public ABPerson [] GetPeopleWithName (string name) var nameHandle = CFString.CreateNative (name); try { var cfArrayRef = ABAddressBookCopyPeopleWithName (Handle, nameHandle); - return NSArray.ArrayFromHandle (cfArrayRef, h => new ABPerson (h, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, h => new ABPerson (h, this), releaseHandle: true); } finally { CFString.ReleaseNative (nameHandle); } @@ -739,7 +739,7 @@ public ABPerson [] GetPeopleWithName (string name) public ABSource []? GetAllSources () { var cfArrayRef = ABAddressBookCopyArrayOfAllSources (GetCheckedHandle ()); - return NSArray.ArrayFromHandle (cfArrayRef, h => new ABSource (h, this), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, h => new ABSource (h, this), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] diff --git a/src/AddressBook/ABGroup.cs b/src/AddressBook/ABGroup.cs index 67df40ed2c08..e42cdd9bc422 100644 --- a/src/AddressBook/ABGroup.cs +++ b/src/AddressBook/ABGroup.cs @@ -219,11 +219,8 @@ IEnumerator IEnumerable.GetEnumerator () public IEnumerator GetEnumerator () { var cfArrayRef = ABGroupCopyArrayOfAllMembers (Handle); - IEnumerable? e = null; - if (cfArrayRef == IntPtr.Zero) - e = new ABRecord [0]; - else - e = NSArray.ArrayFromHandle (cfArrayRef, h => ABRecord.FromHandle (h, AddressBook), releaseHandle: true); + IEnumerable e; + e = NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, h => ABRecord.FromHandle (h, AddressBook), releaseHandle: true); return e.GetEnumerator (); } @@ -248,9 +245,7 @@ public IEnumerator GetEnumerator () public ABRecord [] GetMembers (ABPersonSortBy sortOrdering) { var cfArrayRef = ABGroupCopyArrayOfAllMembersWithSortOrdering (Handle, sortOrdering); - if (cfArrayRef == IntPtr.Zero) - return new ABRecord [0]; - return NSArray.ArrayFromHandle (cfArrayRef, h => ABRecord.FromHandle (h, AddressBook), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, h => ABRecord.FromHandle (h, AddressBook), releaseHandle: true); } [DllImport (Constants.AddressBookLibrary)] diff --git a/src/AddressBook/ABMultiValue.cs b/src/AddressBook/ABMultiValue.cs index 0bb23505893c..d65b841ad8e4 100644 --- a/src/AddressBook/ABMultiValue.cs +++ b/src/AddressBook/ABMultiValue.cs @@ -319,8 +319,7 @@ public ABPropertyType PropertyType { /// public T [] GetValues () { - return NSArray.ArrayFromHandle (ABMultiValue.CopyArrayOfAllValues (Handle), toManaged, releaseHandle: true) - ?? Array.Empty (); + return NSArray.NonNullArrayFromHandleDropNullElements (ABMultiValue.CopyArrayOfAllValues (Handle), toManaged, releaseHandle: true); } /// diff --git a/src/AppKit/NSAccessibility.cs b/src/AppKit/NSAccessibility.cs index 94b363ceca4e..aeda40297a04 100644 --- a/src/AppKit/NSAccessibility.cs +++ b/src/AppKit/NSAccessibility.cs @@ -198,7 +198,7 @@ public static void PostNotification (NSObject element, NSString notification) var handle = NSAccessibilityUnignoredChildren (originalChildren.Handle); GC.KeepAlive (originalChildren); - return NSArray.ArrayFromHandle (handle); + return NSArray.ArrayFromHandleDropNullElements (handle); } [DllImport (Constants.AppKitLibrary)] @@ -215,7 +215,7 @@ public static void PostNotification (NSObject element, NSString notification) var handle = NSAccessibilityUnignoredChildrenForOnlyChild (originalChild.Handle); GC.KeepAlive (originalChild); - return NSArray.ArrayFromHandle (handle); + return NSArray.ArrayFromHandleDropNullElements (handle); } [DllImport (Constants.AppKitLibrary)] diff --git a/src/AuthenticationServices/PublicPrivateKeyAuthentication.cs b/src/AuthenticationServices/PublicPrivateKeyAuthentication.cs index 9d9dc8b2907e..485180207a75 100644 --- a/src/AuthenticationServices/PublicPrivateKeyAuthentication.cs +++ b/src/AuthenticationServices/PublicPrivateKeyAuthentication.cs @@ -10,7 +10,6 @@ #if !TVOS using CoreGraphics; -using System.Linq; #nullable enable @@ -25,14 +24,14 @@ public static class PublicPrivateKeyAuthentication { public static ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransport []? GetAllSupportedPublicKeyCredentialDescriptorTransports () { - NSString []? nsStringArray = NSArray.ArrayFromHandle (ASAuthorizationAllSupportedPublicKeyCredentialDescriptorTransports ()); + var nsStringArray = NSArray.StringArrayFromHandle (ASAuthorizationAllSupportedPublicKeyCredentialDescriptorTransports ()); if (nsStringArray is null) return null; - ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransport [] asArray = new ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransport [nsStringArray.Count ()]; - for (var i = 0; i < nsStringArray.Count (); i++) { - switch (nsStringArray [i].Description) { + var asArray = new ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransport [nsStringArray.Length]; + for (var i = 0; i < nsStringArray.Length; i++) { + switch (nsStringArray [i]) { case "usb": asArray [i] = ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransport.Usb; break; diff --git a/src/CoreAnimation/CADefs.cs b/src/CoreAnimation/CADefs.cs index 1899ea53ec3d..6405f5cefc2d 100644 --- a/src/CoreAnimation/CADefs.cs +++ b/src/CoreAnimation/CADefs.cs @@ -52,24 +52,14 @@ CGColor CreateColor (NativeHandle p) /// An array of colors defining the gradient. These values can be animated. /// To be added. /// To be added. - public CGColor [] Colors { + public CGColor []? Colors { get { - return NSArray.ArrayFromHandle (_Colors, CreateColor); + return NSArray.ArrayFromHandleDropNullElements (_Colors, CreateColor); } set { - if (value is null) { - _Colors = IntPtr.Zero; - return; - } - - var ptrs = new NativeHandle [value.Length]; - for (int i = 0; i < ptrs.Length; i++) - ptrs [i] = value [i].Handle; - - using (NSArray array = NSArray.FromIntPtrs (ptrs)) { - _Colors = array.Handle; - } + using var array = NSArray.FromIntPtrs (value, NativeObjectExtensions.GetHandle); + _Colors = array.GetHandle (); } } } diff --git a/src/CoreGraphics/CGPath.cs b/src/CoreGraphics/CGPath.cs index 2b8844470b15..19ee8c66c540 100644 --- a/src/CoreGraphics/CGPath.cs +++ b/src/CoreGraphics/CGPath.cs @@ -841,9 +841,7 @@ public void Apply (ApplierFunction func) public CGPath [] GetSeparateComponents (bool evenOddFillRule) { var cfArrayRef = CGPathCreateSeparateComponents (Handle, evenOddFillRule.AsByte ()); - if (cfArrayRef == IntPtr.Zero) - return Array.Empty (); - return NSArray.ArrayFromHandle (cfArrayRef); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef); } [SupportedOSPlatform ("ios16.0")] diff --git a/src/CoreML/MLModel.cs b/src/CoreML/MLModel.cs index 539aef822cf0..a995a4321241 100644 --- a/src/CoreML/MLModel.cs +++ b/src/CoreML/MLModel.cs @@ -18,9 +18,7 @@ public partial class MLModel { public static IMLComputeDeviceProtocol [] AllComputeDevices { get { var ptr = MLAllComputeDevices (); - if (ptr == IntPtr.Zero) - return Array.Empty (); - return NSArray.ArrayFromHandle (ptr); + return NSArray.NonNullArrayFromHandleDropNullElements (ptr); } } } diff --git a/src/CoreML/MLMultiArray.cs b/src/CoreML/MLMultiArray.cs index a2c747cff417..b4c85111cfc0 100644 --- a/src/CoreML/MLMultiArray.cs +++ b/src/CoreML/MLMultiArray.cs @@ -22,7 +22,7 @@ static NSNumber [] ConvertArray (nint [] value) // NSArray => nint[] internal static nint [] ConvertArray (IntPtr handle) { - return NSArray.ArrayFromHandle (handle, (v) => (nint) Messaging.IntPtr_objc_msgSend (v, Selector.GetHandle ("integerValue"))); + return NSArray.NonNullArrayFromHandleDropNullElements (handle, (v) => (nint) Messaging.IntPtr_objc_msgSend (v, Selector.GetHandle ("integerValue"))); } /// To be added. diff --git a/src/CoreServices/LaunchServices.cs b/src/CoreServices/LaunchServices.cs index 33a827dee339..2eac64c70bd7 100644 --- a/src/CoreServices/LaunchServices.cs +++ b/src/CoreServices/LaunchServices.cs @@ -189,7 +189,7 @@ public static NSUrl [] GetApplicationUrlsForUrl (NSUrl url, LSRoles roles = LSRo if (url is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (url)); - var result = NSArray.ArrayFromHandle ( + var result = NSArray.NonNullArrayFromHandleDropNullElements ( LSCopyApplicationURLsForURL (url.Handle, roles), releaseHandle: true ); @@ -260,7 +260,7 @@ public static NSUrl [] GetApplicationUrlsForBundleIdentifier (string bundleIdent var bundleIdentifierHandle = CFString.CreateNative (bundleIdentifier); try { - return NSArray.ArrayFromHandle ( + return NSArray.NonNullArrayFromHandleDropNullElements ( LSCopyApplicationURLsForBundleIdentifier (bundleIdentifierHandle, IntPtr.Zero), releaseHandle: true ); diff --git a/src/CoreText/Adapter.cs b/src/CoreText/Adapter.cs index b9f9fc9ba2d8..af706156e441 100644 --- a/src/CoreText/Adapter.cs +++ b/src/CoreText/Adapter.cs @@ -84,9 +84,7 @@ public static void AssertWritable (NSDictionary dictionary) var cfArrayRef = CFDictionary.GetValue (dictionary.Handle, key.Handle); GC.KeepAlive (dictionary); GC.KeepAlive (key); - if (cfArrayRef == NativeHandle.Zero || CFArray.GetCount (cfArrayRef) == 0) - return new T [0]; - return NSArray.ArrayFromHandle (cfArrayRef, converter); + return NSArray.NonNullArrayFromHandleDropNullElements (cfArrayRef, converter); } public static float? GetSingleValue (IDictionary dictionary, NSObject? key) diff --git a/src/CoreText/CTFont.cs b/src/CoreText/CTFont.cs index 28d674c3c8c9..c6f07569c765 100644 --- a/src/CoreText/CTFont.cs +++ b/src/CoreText/CTFont.cs @@ -3560,9 +3560,7 @@ public CTFontFeatureSettings [] GetFeatureSettings () public CTFontTable [] GetAvailableTables (CTFontTableOptions options) { var cfArrayRef = CTFontCopyAvailableTables (Handle, options); - if (cfArrayRef == IntPtr.Zero) - return Array.Empty (); - return NSArray.ArrayFromHandle (cfArrayRef, v => { + return NSArray.NonNullArrayFromHandle (cfArrayRef, v => { return (CTFontTable) (uint) (IntPtr) v; }, true); } diff --git a/src/CoreText/CTFontManager.cs b/src/CoreText/CTFontManager.cs index fcd4c705ebb1..f36ec20189a7 100644 --- a/src/CoreText/CTFontManager.cs +++ b/src/CoreText/CTFontManager.cs @@ -153,18 +153,6 @@ static NSArray EnsureNonNullArray (object [] items, string name) return NSArray.FromObjects (items); } - static T []? ArrayFromHandle (IntPtr handle, bool releaseAfterUse) where T : class, INativeObject - { - if (handle == IntPtr.Zero) - return null; - try { - return NSArray.ArrayFromHandle (handle); - } finally { - if (releaseAfterUse) - CFObject.CFRelease (handle); - } - } - [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] @@ -198,7 +186,7 @@ static NSArray EnsureNonNullArray (object [] items, string name) return null; GC.KeepAlive (arr); } - return ArrayFromHandle (error_array, releaseAfterUse: true); + return NSArray.ArrayFromHandleDropNullElements (error_array, releaseHandle: true); } } @@ -211,7 +199,7 @@ static unsafe byte TrampolineRegistrationHandler (IntPtr block, /* NSArray */ In if (del is null) return 0; - var rv = del (NSArray.ArrayFromHandle (errors), done == 0 ? false : true); + var rv = del (NSArray.NonNullArrayFromHandleDropNullElements (errors), done == 0 ? false : true); return rv ? (byte) 1 : (byte) 0; } @@ -308,7 +296,7 @@ public static void RegisterFonts (NSUrl [] fontUrls, CTFontManagerScope scope, b return null; GC.KeepAlive (arr); } - return ArrayFromHandle (error_array, releaseAfterUse: true); + return NSArray.ArrayFromHandleDropNullElements (error_array, releaseHandle: true); } } @@ -589,7 +577,7 @@ public unsafe static void UnregisterFontDescriptors (CTFontDescriptor [] fontDes { var p = CTFontManagerCopyRegisteredFontDescriptors (scope, enabled.AsByte ()); // Copy/Create rule - we must release the CFArrayRef - return ArrayFromHandle (p, releaseAfterUse: true); + return NSArray.ArrayFromHandleDropNullElements (p, releaseHandle: true); } #endif @@ -628,7 +616,7 @@ public unsafe static void UnregisterFontDescriptors (CTFontDescriptor [] fontDes var p = CTFontManagerCreateFontDescriptorsFromData (data.Handle); GC.KeepAlive (data); // Copy/Create rule - we must release the CFArrayRef - return ArrayFromHandle (p, releaseAfterUse: true); + return NSArray.ArrayFromHandleDropNullElements (p, releaseHandle: true); } #if __IOS__ @@ -676,7 +664,7 @@ static unsafe void TrampolineRequestFonts (IntPtr block, /* CFArray */ IntPtr fo { var del = BlockLiteral.GetTarget (block); if (del is not null) - del (NSArray.ArrayFromHandle (fontDescriptors)); + del (NSArray.NonNullArrayFromHandleDropNullElements (fontDescriptors)); } [SupportedOSPlatform ("ios13.0")] diff --git a/src/CoreVideo/CVPixelFormatDescription.cs b/src/CoreVideo/CVPixelFormatDescription.cs index 2fb0d853fc12..5a86b8498001 100644 --- a/src/CoreVideo/CVPixelFormatDescription.cs +++ b/src/CoreVideo/CVPixelFormatDescription.cs @@ -281,7 +281,7 @@ static CVPixelFormatDescription () /// Get all the known pixel format types. public static NSNumber [] AllTypes { get { - return NSArray.ArrayFromHandle (CVPixelFormatDescriptionArrayCreateWithAllPixelFormatTypes (IntPtr.Zero), releaseHandle: true); + return NSArray.NonNullArrayFromHandleDropNullElements (CVPixelFormatDescriptionArrayCreateWithAllPixelFormatTypes (IntPtr.Zero), releaseHandle: true); } } diff --git a/src/Foundation/DictionaryContainer.cs b/src/Foundation/DictionaryContainer.cs index f373b9acb8eb..76b82f406d5e 100644 --- a/src/Foundation/DictionaryContainer.cs +++ b/src/Foundation/DictionaryContainer.cs @@ -203,7 +203,7 @@ bool TryGetNativeValue (NativeHandle key, out NativeHandle value) if (!TryGetNativeValue (key, out var value)) return null; - return NSArray.ArrayFromHandle (value); + return NSArray.ArrayFromHandleDropNullElements (value); } /// Returns the nullable array of associated with the specified . diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index 26815d1a9618..236f64c43321 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -413,6 +413,25 @@ public static NSArray FromIntPtrs (NativeHandle [] vals) } } + /// Create an from the specified pointers. + /// Array of pointers (to instances). + /// A delegate to convert each array element to a native handle. + /// If the array is null, is returned. + [return: NotNullIfNotNull (nameof (array))] + internal static NSArray? FromIntPtrs (T []? array, Func getHandle) + { + if (array is null) + return null; + + var handles = new NativeHandle [array.Length]; + for (var i = 0; i < handles.Length; i++) + handles [i] = getHandle (array [i]); + + var rv = FromIntPtrs (handles); + GC.KeepAlive (array); + return rv; + } + internal static nuint GetCount (IntPtr handle) { #if MONOMAC @@ -439,34 +458,22 @@ internal static NativeHandle GetAtIndex (NativeHandle handle, nuint i) return CFArray.StringArrayFromHandle (handle); } #endif // !XAMCORE_5_0 -#nullable disable + /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. /// Parameter type, determines the kind of array returned. /// Pointer (handle) to the unmanaged object. - /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. /// An C# array with the values. /// - /// Use this method to get a set of NSObject arrays from a handle to an NSArray - /// - /// (someHandle); + /// + /// (someHandle); /// ]]> - /// - /// - static public T [] ArrayFromHandle (NativeHandle handle) where T : class, INativeObject + /// + /// + public static T? []? ArrayFromHandle (NativeHandle handle) where T : class, INativeObject { - if (handle == NativeHandle.Zero) - return null; - - var c = GetCount (handle); - T [] ret = new T [c]; - - for (uint i = 0; i < c; i++) { - ret [i] = UnsafeGetItem (handle, i); - } - return ret; + return ArrayFromHandle (handle, false); } /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. @@ -475,7 +482,6 @@ static public T [] ArrayFromHandle (NativeHandle handle) where T : class, INa /// Whether the native NSArray instance should be released before returning or not. /// A C# array with the values. /// - /// Use this method to get a set of NSObject arrays from a handle to an NSArray /// /// (NativeHandle handle) where T : class, INa /// ]]> /// /// - public static T [] ArrayFromHandle (NativeHandle handle, bool releaseHandle) where T : class, INativeObject + public static T? []? ArrayFromHandle (NativeHandle handle, bool releaseHandle) where T : class, INativeObject { - var rv = ArrayFromHandle (handle); - if (releaseHandle && handle != NativeHandle.Zero) - NSObject.DangerousRelease (handle); - return rv; + return ArrayFromHandle (handle, h => Runtime.GetINativeObject (h, false), NSNullBehavior.ConvertToNull, releaseHandle); } - static Array ArrayFromHandle (NativeHandle handle, Type elementType) + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// Method that can create objects of type T from a given IntPtr. + /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. + /// An C# array with the values. + /// + /// + /// (someHandle, myCreator); + /// ]]> + /// + /// + public static T? []? ArrayFromHandle (NativeHandle handle, Converter creator) + { + return ArrayFromHandle (handle, creator, NSNullBehavior.ConvertToNull, false); + } + + /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// Method that can create objects of type T from a given handle. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values, or if the handle is . + public static T? []? ArrayFromHandle (NativeHandle handle, Converter creator, bool releaseHandle) + { + return ArrayFromHandle (handle, creator, NSNullBehavior.ConvertToNull, releaseHandle); + } + + /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// A delegate to convert a native handle to an object of type T. + /// How to handle null and NSNull elements in the native array. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values, or if the handle is . + internal static T? []? ArrayFromHandle (NativeHandle handle, Converter createObject, NSNullBehavior nsNullElementBehavior, bool releaseHandle = false) { if (handle == NativeHandle.Zero) return null; - var c = (int) GetCount (handle); - var rv = Array.CreateInstance (elementType, c); - for (int i = 0; i < c; i++) { - rv.SetValue (UnsafeGetItem (handle, (nuint) i, elementType), i); + try { + var count = GetCount (handle); + var ret = new T? [count]; + nuint nextIndex = 0; + + for (nuint i = 0; i < count; i++) { + var val = GetAtIndex (handle, i); + if (!TryGetItem (val, createObject, nsNullElementBehavior, i, out var value)) + continue; + ret [nextIndex++] = value; + } + + if (nextIndex != count) + Array.Resize (ref ret, (int) nextIndex); + + return ret; + } finally { + if (releaseHandle) + NSObject.DangerousRelease (handle); } - return rv; } + /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// How to handle null and NSNull elements in the native array. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values, or if the handle is . + internal static T? []? ArrayFromHandle (NativeHandle handle, NSNullBehavior nsNullElementBehavior, bool releaseHandle = false) where T : class, INativeObject + { + return ArrayFromHandle (handle, (h) => Runtime.GetINativeObject (h, false), nsNullElementBehavior, releaseHandle); + } + + /// Attempts to get an item from the native array, handling null and NSNull elements according to the specified behavior. + /// The type of the item to create. + /// The native handle of the element. + /// A delegate to convert a native handle to an object of type T. + /// How to handle null and NSNull elements. + /// The index of the element in the source array (used for error messages). + /// When this method returns, contains the created object, or the default value if the element was skipped. + /// if the element should be included in the result array; if it should be skipped. + static bool TryGetItem (NativeHandle elementHandle, Converter createObject, NSNullBehavior nsNullElementBehavior, nuint index, out T? value) + { + value = default (T); + + switch (nsNullElementBehavior) { + case NSNullBehavior.Drop: + if (elementHandle == NativeHandle.Zero) + return false; + if (elementHandle == NSNull.NullHandle) + return false; + value = createObject (elementHandle); + return value is not null; + case NSNullBehavior.DropIfIncompatible: + if (elementHandle == NativeHandle.Zero) + return false; + if (elementHandle == NSNull.NullHandle) { + if (NSNull.Null is T nullT) + value = nullT; + } else { + value = createObject (elementHandle); + } + return value is not null; + case NSNullBehavior.ConvertToNull: + if (elementHandle == NativeHandle.Zero) + return true; + if (elementHandle == NSNull.NullHandle) + return true; + value = createObject (elementHandle); + return true; + case NSNullBehavior.Throw: + if (elementHandle != NSNull.NullHandle && elementHandle != NativeHandle.Zero) + value = createObject (elementHandle); + if (value is null) + throw new InvalidOperationException ($"Invalid null element at index {index}"); + return true; + default: + throw new InvalidOperationException ($"Unknown null behavior: {nsNullElementBehavior}"); + } + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values. Returns an empty array if the handle is . + internal static T? [] NonNullArrayFromHandle (NativeHandle handle, bool releaseHandle = false) where T : class, INativeObject + { + return NonNullArrayFromHandle (handle, NSNullBehavior.ConvertToNull, releaseHandle); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// How to handle null and NSNull elements in the native array. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values. Returns an empty array if the handle is . + internal static T? [] NonNullArrayFromHandle (NativeHandle handle, NSNullBehavior nsNullElementBehavior, bool releaseHandle = false) where T : class, INativeObject + { + var rv = ArrayFromHandle (handle, nsNullElementBehavior, releaseHandle); + return rv ?? Array.Empty (); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// A delegate to convert a native handle to an object of type T. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values. Returns an empty array if the handle is . + internal static T? [] NonNullArrayFromHandle (NativeHandle handle, Converter creator, bool releaseHandle = false) + { + var rv = ArrayFromHandle (handle, creator, NSNullBehavior.ConvertToNull, releaseHandle); + return rv ?? Array.Empty (); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping any null or NSNull elements. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements), or if the handle is . + internal static T []? ArrayFromHandleDropNullElements (NativeHandle handle, bool releaseHandle = false) where T : class, INativeObject + { + return ArrayFromHandleDropNullElements (handle, (h) => Runtime.GetINativeObject (h, false)!, releaseHandle); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping any null or NSNull elements. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// A delegate to convert a native handle to an object of type T. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements), or if the handle is . + internal static T []? ArrayFromHandleDropNullElements (NativeHandle handle, Converter createObject, bool releaseHandle = false) + { + return ArrayFromHandle (handle, createObject, NSNullBehavior.Drop, releaseHandle)!; + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping null elements and guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements). Returns an empty array if the handle is . + internal static T [] NonNullArrayFromHandleDropNullElements (NativeHandle handle, bool releaseHandle = false) where T : class, INativeObject + { + return NonNullArrayFromHandleDropNullElements (handle, (h) => Runtime.GetINativeObject (h, false)!, releaseHandle); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping null elements and guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// A delegate to convert a native handle to an object of type T. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements). Returns an empty array if the handle is . + internal static T [] NonNullArrayFromHandleDropNullElements (NativeHandle handle, Converter createObject, bool releaseHandle = false) + { + return NonNullArrayFromHandleDropNullElements (handle, createObject, NSNullBehavior.Drop, releaseHandle); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping null elements and guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// How to handle null and NSNull elements in the native array. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements). Returns an empty array if the handle is . + internal static T [] NonNullArrayFromHandleDropNullElements (NativeHandle handle, NSNullBehavior nsNullElementBehavior, bool releaseHandle = false) where T : class, INativeObject + { + return NonNullArrayFromHandleDropNullElements (handle, (h) => Runtime.GetINativeObject (h, false)!, nsNullElementBehavior, releaseHandle); + } + + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping null elements and guaranteeing a non-null return value. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// A delegate to convert a native handle to an object of type T. + /// How to handle null and NSNull elements in the native array. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements). Returns an empty array if the handle is . + internal static T [] NonNullArrayFromHandleDropNullElements (NativeHandle handle, Converter createObject, NSNullBehavior nsNullElementBehavior, bool releaseHandle = false) + { + var rv = ArrayFromHandle (handle, createObject, nsNullElementBehavior, releaseHandle); + if (rv is null) + return Array.Empty (); + return rv!; + } + +#nullable disable + static public T [] EnumsFromHandle (NativeHandle handle) where T : struct, IConvertible { if (handle == NativeHandle.Zero) @@ -599,16 +819,7 @@ static public T [] FromArrayNative (NSArray weakArray) where T : class, INati /// static public T [] ArrayFromHandleFunc (NativeHandle handle, Func createObject) { - if (handle == NativeHandle.Zero) - return null; - - var c = GetCount (handle); - T [] ret = new T [c]; - - for (uint i = 0; i < c; i++) - ret [i] = createObject (GetAtIndex (handle, i)); - - return ret; + return ArrayFromHandle (handle, (v) => createObject (v)); } /// Create a managed array from a pointer to a native NSArray instance. @@ -617,12 +828,10 @@ static public T [] ArrayFromHandleFunc (NativeHandle handle, FuncWhether the native NSArray instance should be released before returning or not. public static T [] ArrayFromHandleFunc (NativeHandle handle, Func createObject, bool releaseHandle) { - var rv = ArrayFromHandleFunc (handle, createObject); - if (releaseHandle && handle != NativeHandle.Zero) - NSObject.DangerousRelease (handle); - return rv; + return ArrayFromHandle (handle, (v) => createObject (v), releaseHandle); } +#nullable enable /// Creates a managed array from a pointer to a native NSArray of NSDictionary objects, dropping null and NSNull elements. /// The type of objects to create from the dictionaries. /// The pointer to the native NSArray instance containing NSDictionary objects. @@ -632,35 +841,16 @@ public static T [] ArrayFromHandleFunc (NativeHandle handle, Func /// This method converts a native NSArray of NSDictionary objects into a managed array. Any null or NSNull elements in the source array are skipped, and the resulting array is resized accordingly. /// -#nullable enable internal static T []? DictionaryArrayFromHandleDropNullElements (NativeHandle handle, Func createObjectFromDictionary, bool releaseHandle = false) { if (handle == NativeHandle.Zero) return null; - try { - var count = GetCount (handle); - var ret = new T [count]; - nuint nextIndex = 0; - - for (nuint i = 0; i < count; i++) { - var val = GetAtIndex (handle, i); - if (val == IntPtr.Zero || val == NSNull.NullHandle) - continue; - var dict = Runtime.GetNSObject (val); - if (dict is null) - continue; - ret [nextIndex++] = createObjectFromDictionary (dict); - } - - if (nextIndex != count) - Array.Resize (ref ret, (int) nextIndex); - - return ret; - } finally { - if (releaseHandle) - NSObject.DangerousRelease (handle); - } + return ArrayFromHandleDropNullElements (handle, + (dictionaryHandle) => { + return createObjectFromDictionary (Runtime.GetNSObject (dictionaryHandle)!); + }, + releaseHandle); } /// Creates a managed array from a pointer to a native NSArray of NSDictionary objects, dropping null and NSNull elements. Always returns a non-null array. @@ -679,52 +869,16 @@ internal static T [] NonNullDictionaryArrayFromHandleDropNullElements (Native return Array.Empty (); return rv; } -#nullable disable - - /// Parameter type, determines the kind of array returned. - /// Pointer (handle) to the unmanaged object. - /// Method that can create objects of type T from a given IntPtr. - /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. - /// An C# array with the values. - /// - /// Use this method to get a set of NSObject arrays from a handle to an NSArray. Instead of wrapping the results in NSObjects, the code invokes your method to create the return value. - /// - /// (someHandle, myCreator); - /// ]]> - /// - /// - static public T [] ArrayFromHandle (NativeHandle handle, Converter creator) - { - if (handle == NativeHandle.Zero) - return null; - - var c = GetCount (handle); - T [] ret = new T [c]; - - for (uint i = 0; i < c; i++) - ret [i] = creator (GetAtIndex (handle, i)); - - return ret; - } - - static public T [] ArrayFromHandle (NativeHandle handle, Converter creator, bool releaseHandle) - { - var rv = ArrayFromHandle (handle, creator); - if (releaseHandle && handle != NativeHandle.Zero) - NSObject.DangerousRelease (handle); - return rv; - } // FIXME: before proving a real `this` indexer we need to clean the issues between // NSObject and INativeObject coexistance across all the API (it can not return T) - static T UnsafeGetItem (NativeHandle handle, nuint index) where T : class, INativeObject + /// Gets a single item from a native NSArray at the specified index, without bounds checking. + /// The type of the item to retrieve. + /// Pointer (handle) to the native NSArray. + /// The zero-based index of the element to retrieve. + /// The item at the specified index, or if the element is an NSNull instance. + static T? UnsafeGetItem (NativeHandle handle, nuint index) where T : class, INativeObject { var val = GetAtIndex (handle, index); // A native code could return NSArray with NSNull.Null elements @@ -736,18 +890,7 @@ static T UnsafeGetItem (NativeHandle handle, nuint index) where T : class, IN return Runtime.GetINativeObject (val, false); } - static object UnsafeGetItem (NativeHandle handle, nuint index, Type type) - { - var val = GetAtIndex (handle, index); - // A native code could return NSArray with NSNull.Null elements - // and they should be valid for things like T : NSDate so we handle - // them as just null values inside the array - if (val == NSNull.NullHandle) - return null; - - return Runtime.GetINativeObject (val, false, type); - } - +#nullable disable // can return an INativeObject or an NSObject /// To be added. /// To be added. diff --git a/src/Foundation/NSDictionary_2.cs b/src/Foundation/NSDictionary_2.cs index 1e1a32a39b4b..574928fc33c5 100644 --- a/src/Foundation/NSDictionary_2.cs +++ b/src/Foundation/NSDictionary_2.cs @@ -185,7 +185,7 @@ public Dictionary ToDictionary (Func public TKey [] Keys { get { using (var pool = new NSAutoreleasePool ()) - return NSArray.ArrayFromHandle (_AllKeys ()); + return NSArray.NonNullArrayFromHandleDropNullElements (_AllKeys (), NSNullBehavior.DropIfIncompatible); } } @@ -199,7 +199,7 @@ public TKey [] KeysForObject (TValue obj) ArgumentNullException.ThrowIfNull (obj); using (var pool = new NSAutoreleasePool ()) { - var ret = NSArray.ArrayFromHandle (_AllKeysForObject (obj.Handle)); + var ret = NSArray.NonNullArrayFromHandleDropNullElements (_AllKeysForObject (obj.Handle), NSNullBehavior.DropIfIncompatible); GC.KeepAlive (obj); return ret; } @@ -212,7 +212,7 @@ public TKey [] KeysForObject (TValue obj) public TValue [] Values { get { using (var pool = new NSAutoreleasePool ()) - return NSArray.ArrayFromHandle (_AllValues ()); + return NSArray.NonNullArrayFromHandleDropNullElements (_AllValues (), NSNullBehavior.DropIfIncompatible); } } @@ -232,7 +232,7 @@ public TValue [] ObjectsForKeys (TKey [] keys, TValue marker) using (var pool = new NSAutoreleasePool ()) { var keysArray = NSArray.FromNativeObjects (keys); - var result = NSArray.ArrayFromHandle (_ObjectsForKeys (keysArray.Handle, marker.Handle)); + var result = NSArray.NonNullArrayFromHandleDropNullElements (_ObjectsForKeys (keysArray.Handle, marker.Handle), NSNullBehavior.DropIfIncompatible); GC.KeepAlive (keysArray); GC.KeepAlive (marker); return result; diff --git a/src/Foundation/NSMutableDictionary_2.cs b/src/Foundation/NSMutableDictionary_2.cs index 702407d3034e..5503c4b38485 100644 --- a/src/Foundation/NSMutableDictionary_2.cs +++ b/src/Foundation/NSMutableDictionary_2.cs @@ -128,7 +128,7 @@ public NSMutableDictionary (TKey? key, TValue? value) public TKey [] Keys { get { using (var pool = new NSAutoreleasePool ()) - return NSArray.ArrayFromHandle (_AllKeys ()); + return NSArray.NonNullArrayFromHandleDropNullElements (_AllKeys (), NSNullBehavior.DropIfIncompatible); } } @@ -142,7 +142,7 @@ public TKey [] KeysForObject (TValue obj) ArgumentNullException.ThrowIfNull (obj); using (var pool = new NSAutoreleasePool ()) { - var result = NSArray.ArrayFromHandle (_AllKeysForObject (obj.Handle)); + var result = NSArray.NonNullArrayFromHandleDropNullElements (_AllKeysForObject (obj.Handle), NSNullBehavior.DropIfIncompatible); GC.KeepAlive (obj); return result; } @@ -155,7 +155,7 @@ public TKey [] KeysForObject (TValue obj) public TValue [] Values { get { using (var pool = new NSAutoreleasePool ()) - return NSArray.ArrayFromHandle (_AllValues ()); + return NSArray.NonNullArrayFromHandleDropNullElements (_AllValues (), NSNullBehavior.DropIfIncompatible); } } @@ -174,7 +174,7 @@ public TValue [] ObjectsForKeys (TKey [] keys, TValue marker) return []; var keysArray = NSArray.FromNativeObjects (keys); - var result = NSArray.ArrayFromHandle (_ObjectsForKeys (keysArray.Handle, marker.Handle)); + var result = NSArray.NonNullArrayFromHandleDropNullElements (_ObjectsForKeys (keysArray.Handle, marker.Handle), NSNullBehavior.DropIfIncompatible); GC.KeepAlive (keysArray); GC.KeepAlive (marker); return result; diff --git a/src/Foundation/NSNullBehavior.cs b/src/Foundation/NSNullBehavior.cs new file mode 100644 index 000000000000..1b45796a2da1 --- /dev/null +++ b/src/Foundation/NSNullBehavior.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Foundation; + +enum NSNullBehavior { + /// NSNull values are dropped. + Drop, + /// NSNull values are dropped, unless they're compatible with the target type (NSObject, NSNull, INativeObject, etc.). + DropIfIncompatible, + /// NSNull values are converted to . + ConvertToNull, + /// An exception is thrown. + Throw, +} diff --git a/src/Foundation/NSOrderedSet.cs b/src/Foundation/NSOrderedSet.cs index 41ba53892239..26955fef61c3 100644 --- a/src/Foundation/NSOrderedSet.cs +++ b/src/Foundation/NSOrderedSet.cs @@ -70,7 +70,7 @@ public NSObject this [nint idx] { public T [] ToArray () where T : class, INativeObject { IntPtr nsarr = _ToArray (); - return NSArray.ArrayFromHandle (nsarr); + return NSArray.NonNullArrayFromHandleDropNullElements (nsarr, nsNullElementBehavior: NSNullBehavior.DropIfIncompatible); } /// Creates a new from an array of strongly typed values. diff --git a/src/Foundation/NSSet.cs b/src/Foundation/NSSet.cs index 6aa7ae5584bb..4def13b6525b 100644 --- a/src/Foundation/NSSet.cs +++ b/src/Foundation/NSSet.cs @@ -71,7 +71,7 @@ public NSSet (params string? [] strings) : this (NSArray.FromStrings (strings)) public T [] ToArray () where T : class, INativeObject { IntPtr nsarr = _AllObjects (); - return NSArray.ArrayFromHandle (nsarr); + return NSArray.NonNullArrayFromHandleDropNullElements (nsarr, nsNullElementBehavior: NSNullBehavior.DropIfIncompatible); } /// Creates a new from an array of strongly typed values. diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 5ecaed7f9a69..fa759871d4c3 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -782,8 +782,8 @@ public ServerCertificateCustomValidationCallbackHelper (Func 0 ? certificates [0] : null; + var certificates = ConvertCertificates (secTrust); + var certificate = certificates.Length > 0 ? certificates [0] : null; using X509Chain chain = CreateChain (certificates); SslPolicyErrors sslPolicyErrors = EvaluateSslPolicyErrors (certificate, chain, secTrust); @@ -796,6 +796,8 @@ X509Certificate2 [] ConvertCertificates (SecTrust secTrust) if (SystemVersion.IsAtLeastXcode13) { var originalChain = secTrust.GetCertificateChain (); + if (originalChain is null) + return Array.Empty (); for (int i = 0; i < originalChain.Length; i++) certificates [i] = originalChain [i].ToX509Certificate2 (); } else { diff --git a/src/GameplayKit/GKObstacleGraph.cs b/src/GameplayKit/GKObstacleGraph.cs index 9d8a600d7833..7b599d29e5c7 100644 --- a/src/GameplayKit/GKObstacleGraph.cs +++ b/src/GameplayKit/GKObstacleGraph.cs @@ -16,9 +16,9 @@ public partial class GKObstacleGraph { /// Returns the array of corresponding to the . /// To be added. /// To be added. - public GKGraphNode2D [] GetNodes (GKPolygonObstacle obstacle) + public GKGraphNode2D []? GetNodes (GKPolygonObstacle obstacle) { - return NSArray.ArrayFromHandle (_GetNodes (obstacle)); + return NSArray.ArrayFromHandleDropNullElements (_GetNodes (obstacle)); } } @@ -67,9 +67,9 @@ public GKObstacleGraph (NSCoder coder) : base (coder) /// To be added. /// To be added. /// To be added. - public new NodeType [] GetNodes (GKPolygonObstacle obstacle) + public new NodeType []? GetNodes (GKPolygonObstacle obstacle) { - return NSArray.ArrayFromHandle (_GetNodes (obstacle)); + return NSArray.ArrayFromHandleDropNullElements (_GetNodes (obstacle)); } } } diff --git a/src/GameplayKit/NSArray_GameplayKit.cs b/src/GameplayKit/NSArray_GameplayKit.cs index 056969c23394..7f02af96914d 100644 --- a/src/GameplayKit/NSArray_GameplayKit.cs +++ b/src/GameplayKit/NSArray_GameplayKit.cs @@ -29,7 +29,7 @@ public static T [] GetShuffledArray (this NSArray This, GKRandomSource random { if (randomSource is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (randomSource)); - T [] result = NSArray.ArrayFromHandle (Messaging.IntPtr_objc_msgSend_IntPtr (This.Handle, Selector.GetHandle ("shuffledArrayWithRandomSource:"), randomSource.Handle)); + var result = NSArray.NonNullArrayFromHandleDropNullElements (Messaging.IntPtr_objc_msgSend_IntPtr (This.Handle, Selector.GetHandle ("shuffledArrayWithRandomSource:"), randomSource.Handle)); GC.KeepAlive (This); GC.KeepAlive (randomSource); return result; @@ -43,7 +43,7 @@ public static T [] GetShuffledArray (this NSArray This, GKRandomSource random [Export ("shuffledArray")] public static T [] GetShuffledArray (this NSArray This) where T : class, INativeObject { - T [] result = NSArray.ArrayFromHandle (Messaging.IntPtr_objc_msgSend (This.Handle, Selector.GetHandle ("shuffledArray"))); + var result = NSArray.NonNullArrayFromHandleDropNullElements (Messaging.IntPtr_objc_msgSend (This.Handle, Selector.GetHandle ("shuffledArray"))); GC.KeepAlive (This); return result; } diff --git a/src/MediaPlayer/MPNowPlayingInfoCenter.cs b/src/MediaPlayer/MPNowPlayingInfoCenter.cs index 9f86b3696296..633c7a68b17e 100644 --- a/src/MediaPlayer/MPNowPlayingInfoCenter.cs +++ b/src/MediaPlayer/MPNowPlayingInfoCenter.cs @@ -286,11 +286,11 @@ internal MPNowPlayingInfo (NSDictionary? source) DefaultPlaybackRate = (result as NSNumber)?.DoubleValue; if (TryGetValue (source, MPNowPlayingInfoCenter.PropertyAvailableLanguageOptions, out result)) { - AvailableLanguageOptions = NSArray.ArrayFromHandle (result.Handle); + AvailableLanguageOptions = NSArray.ArrayFromHandleDropNullElements (result.Handle); GC.KeepAlive (result); } if (TryGetValue (source, MPNowPlayingInfoCenter.PropertyCurrentLanguageOptions, out result)) { - CurrentLanguageOptions = NSArray.ArrayFromHandle (result.Handle); + CurrentLanguageOptions = NSArray.ArrayFromHandleDropNullElements (result.Handle); GC.KeepAlive (result); } if (TryGetValue (source, MPNowPlayingInfoCenter.PropertyCollectionIdentifier, out result)) diff --git a/src/Metal/MTLDevice.cs b/src/Metal/MTLDevice.cs index a24dbbfaa543..9a30415ba5fe 100644 --- a/src/Metal/MTLDevice.cs +++ b/src/Metal/MTLDevice.cs @@ -63,12 +63,10 @@ public static IMTLDevice? SystemDefault { [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("ios18.0")] [SupportedOSPlatform ("tvos18.0")] - public static IMTLDevice [] GetAllDevices () + public static IMTLDevice []? GetAllDevices () { var rv = MTLCopyAllDevices (); - var devices = NSArray.ArrayFromHandle (rv); - NSObject.DangerousRelease (rv); - return devices; + return NSArray.ArrayFromHandleDropNullElements (rv, releaseHandle: true); } #if MONOMAC @@ -96,13 +94,10 @@ public static IMTLDevice [] GetAllDevices (MTLDeviceNotificationHandler handler, rv = MTLCopyAllDevicesWithObserver (&observer_handle, &block); } - var obj = NSArray.ArrayFromHandle (rv); - NSObject.DangerousRelease (rv); + // owns: Apple's documentation says "The observer out parameter is returned with a +1 retain count [...]." + observer = Runtime.GetNSObject (observer_handle, owns: true); - observer = Runtime.GetNSObject (observer_handle); - NSObject.DangerousRelease (observer_handle); // Apple's documentation says "The observer out parameter is returned with a +1 retain count [...]." - - return obj; + return NSArray.NonNullArrayFromHandleDropNullElements (rv, releaseHandle: true); } /// To be added. diff --git a/src/NaturalLanguage/NLVectorDictionary.cs b/src/NaturalLanguage/NLVectorDictionary.cs index 5a1854db5b10..737643661756 100644 --- a/src/NaturalLanguage/NLVectorDictionary.cs +++ b/src/NaturalLanguage/NLVectorDictionary.cs @@ -19,14 +19,14 @@ public NLVectorDictionary (NSDictionary dictionary) : base (dictionary) { } - public float [] this [NSString key] { + public float []? this [NSString key] { get { if (key is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (key)); var a = CFDictionary.GetValue (Dictionary.Handle, key.Handle); GC.KeepAlive (key); - return NSArray.ArrayFromHandle (a, input => { + return NSArray.ArrayFromHandleDropNullElements (a, input => { return new NSNumber (input).FloatValue; }); } @@ -41,7 +41,7 @@ public float [] this [NSString key] { } } - public float [] this [string key] { + public float []? this [string key] { get { return this [(NSString) key]; } diff --git a/src/Network/NWEndpoint.cs b/src/Network/NWEndpoint.cs index 98669b67138f..77bb494d20e4 100644 --- a/src/Network/NWEndpoint.cs +++ b/src/Network/NWEndpoint.cs @@ -241,11 +241,6 @@ public NWTxtRecord? TxtRecord { } } - internal NWEndpoint []? FromNSArrayHandle (IntPtr handle) - { - return NSArray.ArrayFromHandle (handle); - } - /// Returns an autoreleased NSArray handle. internal IntPtr ToNSArrayHandle (NWEndpoint [] array) { diff --git a/src/ObjCRuntime/RegistrarHelper.cs b/src/ObjCRuntime/RegistrarHelper.cs index cbe3e442205b..e334ebf10c59 100644 --- a/src/ObjCRuntime/RegistrarHelper.cs +++ b/src/ObjCRuntime/RegistrarHelper.cs @@ -295,7 +295,7 @@ unsafe static void NSArray_string_managed_to_native (IntPtr* ptr, string [] valu *ptr = rv; } - unsafe static void NSArray_native_to_managed (IntPtr* ptr, ref T []? value, ref T []? copy) where T : class, INativeObject + unsafe static void NSArray_native_to_managed (IntPtr* ptr, ref T? []? value, ref T? []? copy) where T : class, INativeObject { if (ptr is not null) { value = NSArray.ArrayFromHandle (*ptr); diff --git a/src/Photos/PHFetchResult.cs b/src/Photos/PHFetchResult.cs index 27132407198b..bdb4fd002ef8 100644 --- a/src/Photos/PHFetchResult.cs +++ b/src/Photos/PHFetchResult.cs @@ -40,10 +40,10 @@ IEnumerator IEnumerable.GetEnumerator () /// Returns the objects at , all of which must be type T. /// To be added. /// To be added. - public T [] ObjectsAt (NSIndexSet indexes) where T : NSObject + public T? [] ObjectsAt (NSIndexSet indexes) where T : NSObject { var nsarr = _ObjectsAt (indexes); - return NSArray.ArrayFromHandle (nsarr); + return NSArray.NonNullArrayFromHandle (nsarr); } } } diff --git a/src/Security/Certificate.cs b/src/Security/Certificate.cs index db58710b6a1b..16406191d629 100644 --- a/src/Security/Certificate.cs +++ b/src/Security/Certificate.cs @@ -534,7 +534,6 @@ public static SecIdentity Import (byte [] data, string password) throw new ArgumentException (nameof (password)); using (var pwstring = new NSString (password)) using (var options = NSMutableDictionary.FromObjectAndKey (pwstring, SecImportExport.Passphrase)) { - NSDictionary [] array; #if __MACOS__ /* There are unfortunate platform differences for SecPKCS12Import: * @@ -609,11 +608,11 @@ public static SecIdentity Import (byte [] data, string password) if (OperatingSystem.IsMacOSVersionAtLeast (15, 0)) options.Add (SecImportExport.ToMemoryOnly, NSNumber.FromBoolean (true)); #endif - SecStatusCode result = SecImportExport.ImportPkcs12 (data, options, out array); + SecStatusCode result = SecImportExport.ImportPkcs12 (data, options, out var array); if (result != SecStatusCode.Success) throw new InvalidOperationException (result.ToString ()); - return new SecIdentity (array [0].LowlevelObjectForKey (SecImportExport.Identity.Handle), false); + return new SecIdentity (array! [0].LowlevelObjectForKey (SecImportExport.Identity.Handle), false); } } diff --git a/src/Security/ImportExport.cs b/src/Security/ImportExport.cs index b7875c50e0bb..a2714e5b7d24 100644 --- a/src/Security/ImportExport.cs +++ b/src/Security/ImportExport.cs @@ -45,7 +45,7 @@ public partial class SecImportExport { /// To be added. /// To be added. /// To be added. - static public SecStatusCode ImportPkcs12 (byte [] buffer, NSDictionary options, out NSDictionary [] array) + static public SecStatusCode ImportPkcs12 (byte [] buffer, NSDictionary options, out NSDictionary []? array) { using (NSData data = NSData.FromArray (buffer)) { return ImportPkcs12 (data, options, out array); @@ -58,7 +58,7 @@ static public SecStatusCode ImportPkcs12 (byte [] buffer, NSDictionary options, /// To be added. /// To be added. /// To be added. - static public SecStatusCode ImportPkcs12 (NSData data, NSDictionary options, out NSDictionary [] array) + static public SecStatusCode ImportPkcs12 (NSData data, NSDictionary options, out NSDictionary []? array) { if (options is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (options)); @@ -70,8 +70,7 @@ static public SecStatusCode ImportPkcs12 (NSData data, NSDictionary options, out GC.KeepAlive (data); GC.KeepAlive (options); } - array = NSArray.ArrayFromHandle (handle); - NSObject.DangerousRelease (handle); + array = NSArray.ArrayFromHandleDropNullElements (handle, releaseHandle: true); return code; } } diff --git a/src/Security/SecIdentity2.cs b/src/Security/SecIdentity2.cs index a26bef4aaea5..d5806ffa9d98 100644 --- a/src/Security/SecIdentity2.cs +++ b/src/Security/SecIdentity2.cs @@ -78,14 +78,10 @@ public SecIdentity2 (SecIdentity identity, params SecCertificate [] certificates /// To be added. /// To be added. /// To be added. - public SecCertificate [] Certificates { + public SecCertificate []? Certificates { get { var certArray = sec_identity_copy_certificates_ref (GetCheckedHandle ()); - try { - return NSArray.ArrayFromHandle (certArray); - } finally { - CFObject.CFRelease (certArray); - } + return NSArray.ArrayFromHandleDropNullElements (certArray, releaseHandle: true); } } diff --git a/src/Security/SecTrust.cs b/src/Security/SecTrust.cs index c6d260aa38d2..ba83a42778d5 100644 --- a/src/Security/SecTrust.cs +++ b/src/Security/SecTrust.cs @@ -52,7 +52,7 @@ public SecTrust (SecCertificate certificate, SecPolicy policy) [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("tvos")] - public SecPolicy [] GetPolicies () + public SecPolicy []? GetPolicies () { IntPtr p = IntPtr.Zero; SecStatusCode result; @@ -61,7 +61,7 @@ public SecPolicy [] GetPolicies () } if (result != SecStatusCode.Success) throw new InvalidOperationException (result.ToString ()); - return NSArray.ArrayFromHandle (p, releaseHandle: true); + return NSArray.ArrayFromHandleDropNullElements (p, releaseHandle: true); } [DllImport (Constants.SecurityLibrary)] @@ -164,7 +164,7 @@ public bool NetworkFetchAllowed { [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("tvos")] - public SecCertificate [] GetCustomAnchorCertificates () + public SecCertificate []? GetCustomAnchorCertificates () { IntPtr p; SecStatusCode result; @@ -173,7 +173,7 @@ public SecCertificate [] GetCustomAnchorCertificates () } if (result != SecStatusCode.Success) throw new InvalidOperationException (result.ToString ()); - return NSArray.ArrayFromHandle (p, releaseHandle: true); + return NSArray.ArrayFromHandleDropNullElements (p, releaseHandle: true); } [SupportedOSPlatform ("ios")] diff --git a/src/Security/Trust.cs b/src/Security/Trust.cs index f9102014cd83..526d223540e3 100644 --- a/src/Security/Trust.cs +++ b/src/Security/Trust.cs @@ -238,8 +238,8 @@ public SecCertificate this [nint index] { [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("ios15.0")] [SupportedOSPlatform ("maccatalyst")] - public SecCertificate [] GetCertificateChain () - => NSArray.ArrayFromHandle (SecTrustCopyCertificateChain (Handle), releaseHandle: true); + public SecCertificate []? GetCertificateChain () + => NSArray.ArrayFromHandleDropNullElements (SecTrustCopyCertificateChain (Handle), releaseHandle: true); [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] diff --git a/src/UIKit/UICellAccessory.cs b/src/UIKit/UICellAccessory.cs index a743fbb1aa7e..9e404c6c0756 100644 --- a/src/UIKit/UICellAccessory.cs +++ b/src/UIKit/UICellAccessory.cs @@ -67,7 +67,7 @@ static unsafe nuint Invoke (IntPtr block, IntPtr accessories) var del = BlockLiteral.GetTarget (block); if (del is null) return default; - nuint retval = del (NSArray.ArrayFromHandle (accessories)); + nuint retval = del (NSArray.NonNullArrayFromHandleDropNullElements (accessories)); return retval; } } /* class SDUICellAccessoryPosition */ diff --git a/src/VideoToolbox/VTRawProcessingSession.cs b/src/VideoToolbox/VTRawProcessingSession.cs index 3cc095d2b929..334720681b79 100644 --- a/src/VideoToolbox/VTRawProcessingSession.cs +++ b/src/VideoToolbox/VTRawProcessingSession.cs @@ -134,7 +134,7 @@ static void VTRawProcessingParameterChangeHandlerCallback (IntPtr block, IntPtr { var del = BlockLiteral.GetTarget (block); if (del is not null) { - var newParams = NSArray.ArrayFromHandle (newParameters); + var newParams = NSArray.ArrayFromHandleDropNullElements (newParameters); del (newParams); } } @@ -197,11 +197,8 @@ unsafe static extern VTStatus VTRAWProcessingSessionCopyProcessingParameters ( unsafe { status = VTRAWProcessingSessionCopyProcessingParameters (GetCheckedHandle (), &handle); } - if (status == VTStatus.Ok && handle != IntPtr.Zero) { - var rv = NSArray.ArrayFromHandle (handle)!; - NSObject.DangerousRelease (handle); // owns: true - return rv; - } + if (status == VTStatus.Ok) + return NSArray.ArrayFromHandleDropNullElements (handle, releaseHandle: true); return null; } diff --git a/src/Vision/VNRequest.cs b/src/Vision/VNRequest.cs index 83b2893a7870..d1ed9e9a51bb 100644 --- a/src/Vision/VNRequest.cs +++ b/src/Vision/VNRequest.cs @@ -26,13 +26,13 @@ public partial class VNRequest { /// ]]> /// /// - public virtual T [] GetResults () where T : VNObservation + public virtual T []? GetResults () where T : VNObservation { // From docs: If the request failed, this property will be nil; // otherwise, it will be an array of zero or more VNObservation // subclasses specific to the VNRequest subclass. // ArrayFromHandle does the null checking for us. - return NSArray.ArrayFromHandle (_Results); + return NSArray.ArrayFromHandleDropNullElements (_Results); } } } diff --git a/src/frameworks.sources b/src/frameworks.sources index 049ccf36f46a..50e4ca60490e 100644 --- a/src/frameworks.sources +++ b/src/frameworks.sources @@ -870,6 +870,7 @@ FOUNDATION_SOURCES = \ Foundation/NSNetService.cs \ Foundation/NSNotificationCenter.cs \ Foundation/NSNull.cs \ + Foundation/NSNullBehavior.cs \ Foundation/NSOrderedSet.cs \ Foundation/NSOrderedSet_1.cs \ Foundation/NSOutputStream.cs \ diff --git a/system-dependencies.sh b/system-dependencies.sh index 26f0b38b9eb0..292a7c8c12eb 100755 --- a/system-dependencies.sh +++ b/system-dependencies.sh @@ -721,12 +721,26 @@ function check_xcode_components () for comp in "${COMPONENTS[@]}"; do componentInfo=$(xcrun xcodebuild -showComponent "$comp") - if [[ "$componentInfo" =~ .*Status:" "installed.* ]]; then + local NEEDS_INSTALL= + local NEEDS_UPDATE= + if [[ "$componentInfo" =~ .*Status:" "installedUpdateAvailable.* ]]; then + NEEDS_UPDATE=1 + elif [[ "$componentInfo" =~ .*Status:" "installed.* ]]; then + NEEDS_INSTALL= + else + NEEDS_INSTALL=1 + fi + + if test -z "$NEEDS_INSTALL$NEEDS_UPDATE"; then ok "The Xcode component ${COLOR_BLUE}$comp${COLOR_CLEAR} is installed." elif test -z "$PROVISION_XCODE_COMPONENTS"; then - fail "The Xcode component ${COLOR_BLUE}$comp${COLOR_RESET} is not installed. Execute ${COLOR_MAGENTA}xcrun xcodebuild -downloadComponent $comp${COLOR_RESET} or ${COLOR_MAGENTA}./system-dependencies.sh --provision-xcode-components${COLOR_RESET} to install." + if test -n "$NEEDS_UPDATE"; then + fail "The Xcode component ${COLOR_BLUE}$comp${COLOR_RESET} is installed, but an update is available. Execute ${COLOR_MAGENTA}xcrun xcodebuild -downloadComponent $comp${COLOR_RESET} or ${COLOR_MAGENTA}./system-dependencies.sh --provision-xcode-components${COLOR_RESET} to install." + else + fail "The Xcode component ${COLOR_BLUE}$comp${COLOR_RESET} is not installed. Execute ${COLOR_MAGENTA}xcrun xcodebuild -downloadComponent $comp${COLOR_RESET} or ${COLOR_MAGENTA}./system-dependencies.sh --provision-xcode-components${COLOR_RESET} to install." + fi fail "Alternatively you can ${COLOR_MAGENTA}export IGNORE_XCODE_COMPONENTS=1${COLOR_RED} to skip this check." - else + elif test -n "$PROVISION_XCODE_COMPONENTS"; then log "Installing the Xcode component ${COLOR_BLUE}$comp${COLOR_CLEAR} by executing ${COLOR_BLUE}xcrun xcodebuild -downloadComponent $comp${COLOR_CLEAR}..." xcrun xcodebuild -downloadComponent "$comp" diff --git a/tests/cecil-tests/Documentation.KnownFailures.txt b/tests/cecil-tests/Documentation.KnownFailures.txt index 028cfab1de5e..267086a2c81b 100644 --- a/tests/cecil-tests/Documentation.KnownFailures.txt +++ b/tests/cecil-tests/Documentation.KnownFailures.txt @@ -11800,7 +11800,6 @@ M:Foundation.INSUrlSessionTaskDelegate.NeedNewBodyStream(Foundation.NSUrlSession M:Foundation.INSUrlSessionWebSocketDelegate.DidClose(Foundation.NSUrlSession,Foundation.NSUrlSessionWebSocketTask,Foundation.NSUrlSessionWebSocketCloseCode,Foundation.NSData) M:Foundation.INSUrlSessionWebSocketDelegate.DidOpen(Foundation.NSUrlSession,Foundation.NSUrlSessionWebSocketTask,System.String) M:Foundation.INSXpcListenerDelegate.ShouldAcceptConnection(Foundation.NSXpcListener,Foundation.NSXpcConnection) -M:Foundation.NSArray.ArrayFromHandle``1(ObjCRuntime.NativeHandle,System.Converter{ObjCRuntime.NativeHandle,``0},System.Boolean) M:Foundation.NSArray.EnumsFromHandle``1(ObjCRuntime.NativeHandle) M:Foundation.NSArray.ToArray M:Foundation.NSArray.ToArray``1 diff --git a/tests/cecil-tests/HandleSafety.KnownFailures.cs b/tests/cecil-tests/HandleSafety.KnownFailures.cs index e9b27a43d5c2..403ad70a55bd 100644 --- a/tests/cecil-tests/HandleSafety.KnownFailures.cs +++ b/tests/cecil-tests/HandleSafety.KnownFailures.cs @@ -15,7 +15,6 @@ public partial class HandleSafetyTest { "AudioUnit.AUScheduledAudioFileRegion.GetAudioFileRegion ()", "AudioUnit.SamplerInstrumentData.ToStruct ()", "AVFoundation.AVCaptureReactionType_Extensions.GetSystemImage (AVFoundation.AVCaptureReactionType)", - "CoreAnimation.CAGradientLayer.set_Colors (CoreGraphics.CGColor[])", "CoreFoundation.CFArray.Create (ObjCRuntime.INativeObject[])", "CoreFoundation.CFDataBuffer.get_Handle ()", "CoreFoundation.CFDictionary.FromObjectsAndKeys (ObjCRuntime.INativeObject[], ObjCRuntime.INativeObject[])", diff --git a/tests/common/DotNet.cs b/tests/common/DotNet.cs index 914cd28c1440..0d0b0de77088 100644 --- a/tests/common/DotNet.cs +++ b/tests/common/DotNet.cs @@ -64,9 +64,10 @@ public static ExecutionResult AssertBuild (string project, Dictionary? properties = null, TimeSpan? timeout = null) + public static ExecutionResult AssertRun (string project, Dictionary? properties = null, TimeSpan? timeout = null, Dictionary? environmentVariables = null) { - return Execute ("run", project, properties, true, timeout: timeout); + var extraArguments = environmentVariables?.SelectMany (kvp => new string [] { "-e", $"{kvp.Key}={kvp.Value}" })?.ToArray () ?? []; + return Execute ("run", project, properties, true, timeout: timeout, extraArguments: extraArguments); } public static ExecutionResult AssertBuildFailure (string project, Dictionary? properties = null) @@ -208,7 +209,7 @@ public static ExecutionResult ExecuteCommand (string exe, Dictionary? properties, bool assert_success = true, string? target = null, bool? msbuildParallelism = null, TimeSpan? timeout = null) + public static ExecutionResult Execute (string verb, string project, Dictionary? properties, bool assert_success = true, string? target = null, bool? msbuildParallelism = null, TimeSpan? timeout = null, params string [] extraArguments) { if (!File.Exists (project)) throw new FileNotFoundException ($"The project file '{project}' does not exist."); @@ -301,6 +302,7 @@ public static ExecutionResult Execute (string verb, string project, Dictionary (); env ["MSBuildSDKsPath"] = null; diff --git a/tests/dotnet/MyRunApp/AppDelegate.cs b/tests/dotnet/MyRunApp/AppDelegate.cs index 36180971144c..2a1b64386bd9 100644 --- a/tests/dotnet/MyRunApp/AppDelegate.cs +++ b/tests/dotnet/MyRunApp/AppDelegate.cs @@ -34,6 +34,12 @@ static int Main (string [] args) } File.WriteAllText (filename, sb.ToString ()); + return 0; + case 2: + foreach (var kvp in Environment.GetEnvironmentVariables ().Cast ().OrderBy (v => v.Key)) { + Console.WriteLine ($"{kvp.Key}={kvp.Value}"); + } + return 0; } diff --git a/tests/dotnet/UnitTests/IncrementalBuildTest.cs b/tests/dotnet/UnitTests/IncrementalBuildTest.cs index 5ce942c4c7f7..fbe0736b6bc1 100644 --- a/tests/dotnet/UnitTests/IncrementalBuildTest.cs +++ b/tests/dotnet/UnitTests/IncrementalBuildTest.cs @@ -205,6 +205,57 @@ public void CodeChangeSkipsTargetsOnRemoteWindows (ApplePlatform platform, strin CodeChangeSkipsTargetsImpl (platform, runtimeIdentifiers, interpreterEnabled); } + [Test] + [TestCase (ApplePlatform.iOS, "iossimulator-arm64")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")] + public void MetalShadersNotRecompiled (ApplePlatform platform, string runtimeIdentifiers) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GenerateProject (platform, name: nameof (MetalShadersNotRecompiled), runtimeIdentifiers: runtimeIdentifiers, out var appPath); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["UseInterpreter"] = "true"; // this makes the test faster + + var projectDir = Path.GetDirectoryName (project_path)!; + + // Add a Main.cs so the project compiles + File.WriteAllText (Path.Combine (projectDir, "Main.cs"), @" +class MainClass { + static int Main () + { + return 0; + } +} +"); + + // Add a Metal shader file to the project + File.WriteAllText (Path.Combine (projectDir, "Shaders.metal"), @" +#include +using namespace metal; + +kernel void myKernel (texture2d inTexture [[texture(0)]], + texture2d outTexture [[texture(1)]], + uint2 gid [[thread_position_in_grid]]) +{ +} +"); + + // Build the first time + var rv = DotNet.AssertBuild (project_path, properties); + var allTargets = BinLog.GetAllTargets (rv.BinLogPath); + AssertTargetExecuted (allTargets, "_SmeltMetal", "First build"); + AssertTargetExecuted (allTargets, "_TemperMetal", "First build"); + + // Build again without any changes + rv = DotNet.AssertBuild (project_path, properties); + allTargets = BinLog.GetAllTargets (rv.BinLogPath); + + // _SmeltMetal should NOT execute on the second build since nothing changed + AssertTargetNotExecuted (allTargets, "_SmeltMetal", "Second build"); + AssertTargetNotExecuted (allTargets, "_TemperMetal", "Second build"); + } + void CodeChangeSkipsTargetsImpl (ApplePlatform platform, string runtimeIdentifiers, bool interpreterEnabled) { var project = "IncrementalTestApp"; diff --git a/tests/dotnet/UnitTests/ProjectTest.cs b/tests/dotnet/UnitTests/ProjectTest.cs index e0062982c7b1..b0b5696427ef 100644 --- a/tests/dotnet/UnitTests/ProjectTest.cs +++ b/tests/dotnet/UnitTests/ProjectTest.cs @@ -3861,9 +3861,11 @@ public void HttpClientHandlerFeatureTrimmedAway (ApplePlatform platform, string } } - [TestCase (ApplePlatform.MacCatalyst)] - [TestCase (ApplePlatform.MacOSX)] - public void Run (ApplePlatform platform) + [TestCase (ApplePlatform.MacCatalyst, true)] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.MacOSX, true)] + [TestCase (ApplePlatform.MacOSX, false)] + public void Run (ApplePlatform platform, bool dotnetRunEnvironmentSupport) { var project = "MyRunApp"; Configuration.IgnoreIfIgnoredPlatform (platform); @@ -3877,6 +3879,7 @@ public void Run (ApplePlatform platform) var stderr = Path.Combine (tmpdir, "stderr.txt"); var properties = GetDefaultProperties (); + var dotnetRunEnvironment = new Dictionary (); properties ["XamarinDebugMode"] = "telegraph"; properties ["XamarinDebugHosts"] = "localhost"; properties ["XamarinDebugPort"] = "123"; @@ -3884,8 +3887,14 @@ public void Run (ApplePlatform platform) properties ["StandardErrorPath"] = stderr; properties ["OpenNewInstance"] = "true"; properties ["OpenWaitForExit"] = "true"; - properties ["RunEnvironment"] = $"--env TEST_CASE=1 --env VARIABLE=VALUE --env TEST_FILENAME={tmpfile}"; - DotNet.AssertRun (project_path, properties); + if (dotnetRunEnvironmentSupport) { + properties ["RunEnvironment"] = $"--env TEST_CASE=1 --env VARIABLE=VALUE --env TEST_FILENAME={tmpfile}"; + } else { + dotnetRunEnvironment ["TEST_CASE"] = "1"; + dotnetRunEnvironment ["VARIABLE"] = "VALUE"; + dotnetRunEnvironment ["TEST_FILENAME"] = tmpfile; + } + DotNet.AssertRun (project_path, properties, environmentVariables: dotnetRunEnvironment); Assert.Multiple (() => { var envContents = File.ReadAllText (tmpfile); diff --git a/tests/dotnet/UnitTests/WindowsTest.cs b/tests/dotnet/UnitTests/WindowsTest.cs index 0599eae94308..f3afb016950e 100644 --- a/tests/dotnet/UnitTests/WindowsTest.cs +++ b/tests/dotnet/UnitTests/WindowsTest.cs @@ -272,6 +272,27 @@ static void AssertWarningsEqual (IList expected, IList actual, s Assert.Fail (sb.ToString ()); } + [Category ("RemoteWindows")] + [TestCase (ApplePlatform.iOS, "ios-arm64", "Release")] + public void StripTest (ApplePlatform platform, string runtimeIdentifiers, string configuration) + { + var project = "MySimpleApp"; + + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + Configuration.IgnoreIfNotOnWindows (); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + var project_dir = Path.GetDirectoryName (project_path)!; + Clean (project_path); + + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["Configuration"] = configuration; + properties ["_ExportSymbolsExplicitly"] = "false"; + + DotNet.AssertBuild (project_path, properties, timeout: TimeSpan.FromMinutes (15)); + } + [Category ("RemoteWindows")] [TestCase (ApplePlatform.iOS, "ios-arm64")] public void RemoteTest (ApplePlatform platform, string runtimeIdentifiers) diff --git a/tests/monotouch-test/CoreAnimation/CAGradientLayerTest.cs b/tests/monotouch-test/CoreAnimation/CAGradientLayerTest.cs new file mode 100644 index 000000000000..41b3056a928a --- /dev/null +++ b/tests/monotouch-test/CoreAnimation/CAGradientLayerTest.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +// Unit tests for CAGradientLayer +// + +using CoreAnimation; +using CoreGraphics; + +namespace MonoTouchFixtures.CoreAnimation { + + [TestFixture] + [Preserve (AllMembers = true)] + public class CAGradientLayerTest { + + [Test] + public void Colors_GetSet () + { + using var layer = new CAGradientLayer (); + Assert.IsNull (layer.Colors, "Colors/default"); + + var red = new CGColor (1, 0, 0); + var green = new CGColor (0, 1, 0); + var blue = new CGColor (0, 0, 1); + layer.Colors = new CGColor [] { red, green, blue }; + var colors = layer.Colors; + Assert.IsNotNull (colors, "Colors/assigned"); + Assert.AreEqual (3, colors!.Length, "Colors/length"); + + layer.Colors = null; + Assert.IsNull (layer.Colors, "Colors/null"); + } + } +} diff --git a/tests/monotouch-test/CoreML/MLModelTest.cs b/tests/monotouch-test/CoreML/MLModelTest.cs new file mode 100644 index 000000000000..ef1ed7ea628c --- /dev/null +++ b/tests/monotouch-test/CoreML/MLModelTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +// Unit tests for MLModel +// + +using CoreML; + +namespace MonoTouchFixtures.CoreML { + + [TestFixture] + [Preserve (AllMembers = true)] + public class MLModelTest { + + [Test] + public void AllComputeDevices () + { + TestRuntime.AssertXcodeVersion (15, 0); + + var devices = MLModel.AllComputeDevices; + Assert.IsNotNull (devices, "AllComputeDevices"); + Assert.That (devices.Length, Is.GreaterThanOrEqualTo (1), "AllComputeDevices/length"); + } + } +} diff --git a/tests/monotouch-test/CoreText/CTFontGetAvailableTablesTest.cs b/tests/monotouch-test/CoreText/CTFontGetAvailableTablesTest.cs new file mode 100644 index 000000000000..ff8e83e6dde5 --- /dev/null +++ b/tests/monotouch-test/CoreText/CTFontGetAvailableTablesTest.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +// Unit tests for CTFont.GetAvailableTables +// + +using CoreText; + +namespace MonoTouchFixtures.CoreText { + + [TestFixture] + [Preserve (AllMembers = true)] + public class CTFontGetAvailableTablesTest { + + [Test] + public void GetAvailableTables () + { + using var font = new CTFont ("Helvetica", 12); + var tables = font.GetAvailableTables (CTFontTableOptions.None); + Assert.IsNotNull (tables, "tables"); + Assert.That (tables.Length, Is.GreaterThan (0), "tables/length"); + } + } +} diff --git a/tests/monotouch-test/GameplayKit/GKObstacleGraphTest.cs b/tests/monotouch-test/GameplayKit/GKObstacleGraphTest.cs new file mode 100644 index 000000000000..e0781d060ed5 --- /dev/null +++ b/tests/monotouch-test/GameplayKit/GKObstacleGraphTest.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +// Unit tests for GKObstacleGraph +// + +using System.Numerics; +using GameplayKit; + +namespace MonoTouchFixtures.GameplayKit { + + [TestFixture] + [Preserve (AllMembers = true)] + public class GKObstacleGraphTest { + + [Test] + public void GetNodes_ReturnsNullForUnknownObstacle () + { + TestRuntime.AssertXcodeVersion (7, 0); + + var points = new [] { + new Vector2 (0, 0), + new Vector2 (10, 0), + new Vector2 (10, 10), + }; + var obstacle = GKPolygonObstacle.FromPoints (points); + var graph = GKObstacleGraph.FromObstacles (new GKPolygonObstacle [] { obstacle }, 1.0f); + Assert.IsNotNull (graph, "graph"); + + var nodes = graph!.GetNodes (obstacle); + // May return null or a valid array depending on the graph state + // The key thing is it doesn't crash + if (nodes is not null) + Assert.That (nodes.Length, Is.GreaterThanOrEqualTo (0), "nodes/length"); + + // Query for an obstacle not in the graph + var otherObstacle = GKPolygonObstacle.FromPoints (new [] { + new Vector2 (100, 100), + new Vector2 (110, 100), + new Vector2 (110, 110), + }); + var otherNodes = graph.GetNodes (otherObstacle); + // An obstacle not in the graph may return null or an empty array + if (otherNodes is not null) + Assert.AreEqual (0, otherNodes.Length, "otherNodes/empty"); + } + } +} diff --git a/tests/monotouch-test/GameplayKit/NSArrayGameplayKitTest.cs b/tests/monotouch-test/GameplayKit/NSArrayGameplayKitTest.cs new file mode 100644 index 000000000000..971f06e6ef24 --- /dev/null +++ b/tests/monotouch-test/GameplayKit/NSArrayGameplayKitTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +// Unit tests for NSArray_GameplayKit +// + +using GameplayKit; + +namespace MonoTouchFixtures.GameplayKit { + + [TestFixture] + [Preserve (AllMembers = true)] + public class NSArrayGameplayKitTest { + + [Test] + public void GetShuffledArray_WithRandomSource () + { + TestRuntime.AssertXcodeVersion (8, 0); + + var array = NSArray.FromNSObjects ( + (NSString) "a", + (NSString) "b", + (NSString) "c", + (NSString) "d", + (NSString) "e" + ); + var randomSource = new GKMersenneTwisterRandomSource (); + + var shuffled = array.GetShuffledArray (randomSource); + Assert.IsNotNull (shuffled, "shuffled"); + Assert.AreEqual (5, shuffled.Length, "shuffled/length"); + } + + [Test] + public void GetShuffledArray_NoArgs () + { + TestRuntime.AssertXcodeVersion (8, 0); + + var array = NSArray.FromNSObjects ( + (NSString) "a", + (NSString) "b", + (NSString) "c" + ); + + var shuffled = array.GetShuffledArray (); + Assert.IsNotNull (shuffled, "shuffled"); + Assert.AreEqual (3, shuffled.Length, "shuffled/length"); + } + } +} diff --git a/tests/monotouch-test/Security/IdentityTest.cs b/tests/monotouch-test/Security/IdentityTest.cs index a6e1084901a4..32e03316aaac 100644 --- a/tests/monotouch-test/Security/IdentityTest.cs +++ b/tests/monotouch-test/Security/IdentityTest.cs @@ -66,5 +66,16 @@ public void AccessCertificates () Assert.That (call, Is.EqualTo (1), "call"); } } + + [Test] + public void Certificates () + { + TestRuntime.AssertXcodeVersion (11, 0); + using var i1 = GetIdentity (); + using var i2 = new SecIdentity2 (i1, i1.Certificate); + var certs = i2.Certificates; + Assert.IsNotNull (certs, "Certificates"); + Assert.That (certs!.Length, Is.GreaterThanOrEqualTo (1), "Certificates/length"); + } } } diff --git a/tests/monotouch-test/Vision/VNRequestGetResultsTest.cs b/tests/monotouch-test/Vision/VNRequestGetResultsTest.cs new file mode 100644 index 000000000000..e8350f1056f5 --- /dev/null +++ b/tests/monotouch-test/Vision/VNRequestGetResultsTest.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +// Unit tests for VNRequest.GetResults +// + +using CoreGraphics; +using Vision; + +namespace MonoTouchFixtures.Vision { + + [TestFixture] + [Preserve (AllMembers = true)] + public class VNRequestGetResultsTest { + + [Test] + public void GetResults_BeforePerform_ReturnsNull () + { + TestRuntime.AssertXcodeVersion (11, 0); + + using var request = new VNDetectFaceRectanglesRequest ((request, error) => { }); + var results = request.GetResults (); + Assert.IsNull (results, "GetResults/before-perform"); + } + + [Test] + public void GetResults_AfterPerform () + { + TestRuntime.AssertXcodeVersion (11, 0); + + using var request = new VNDetectRectanglesRequest ((request, error) => { }); + // Create a simple 100x100 white image + var colorSpace = CGColorSpace.CreateDeviceRGB (); + using var context = new CGBitmapContext (null, 100, 100, 8, 400, colorSpace, CGImageAlphaInfo.PremultipliedLast); + context.SetFillColor (new CGColor (1, 1, 1)); + context.FillRect (new CGRect (0, 0, 100, 100)); + using var image = context.ToImage ()!; + + using var handler = new VNImageRequestHandler (image, new NSDictionary ()); + handler.Perform (new VNRequest [] { request }, out var error); + + // Results may be empty but should not be null after performing + var results = request.GetResults (); + Assert.IsNotNull (results, "GetResults/after-perform"); + } + } +}