diff --git a/Xamarin.MacDev/XcodeLocator.cs b/Xamarin.MacDev/XcodeLocator.cs index ec4ab66..0c68fe5 100644 --- a/Xamarin.MacDev/XcodeLocator.cs +++ b/Xamarin.MacDev/XcodeLocator.cs @@ -125,7 +125,7 @@ public bool TryLocatingXcode (string? xcodeLocationOverride) } // 3. Not optional - if (TryGetSystemXcode (log, out location)) { + if (TryGetSystemXcode (log, out var systemXcodePath) && TryLocatingSpecificXcode (systemXcodePath, out location)) { log.LogInfo ($"Found a valid Xcode from the system settings ('xcode-select -p')."); XcodeLocation = location; return true; diff --git a/tests/XcodeLocatorTests.cs b/tests/XcodeLocatorTests.cs new file mode 100644 index 0000000..893bdd0 --- /dev/null +++ b/tests/XcodeLocatorTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.IO; + +using NUnit.Framework; + +using Xamarin.MacDev; + +namespace Tests { + + [TestFixture] + [Platform (Exclude = "Win", Reason = "Xcode is a macOS-only tool; xcode-select and Xcode bundles are not valid on Windows.")] + public class XcodeLocatorTests { + + static string CreateFakeXcodeBundle (string version = "16.2", string dtXcode = "1620", string? cfBundleVersion = null) + { + var dir = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName () + ".app"); + var contentsDir = Path.Combine (dir, "Contents"); + Directory.CreateDirectory (contentsDir); + + var buildVersion = cfBundleVersion ?? "16C5032a"; + + File.WriteAllText (Path.Combine (contentsDir, "version.plist"), $@" + + + + CFBundleShortVersionString + {version} + CFBundleVersion + {buildVersion} + + +"); + + File.WriteAllText (Path.Combine (contentsDir, "Info.plist"), $@" + + + + DTXcode + {dtXcode} + + +"); + + return dir; + } + + [Test] + public void TryLocatingXcode_Override_PopulatesXcodeVersion () + { + var fakeXcode = CreateFakeXcodeBundle (version: "16.2", dtXcode: "1620"); + try { + var locator = new XcodeLocator (ConsoleLogger.Instance); + var found = locator.TryLocatingXcode (fakeXcode); + Assert.That (found, Is.True, "TryLocatingXcode should return true for a valid Xcode bundle."); + Assert.That (locator.XcodeVersion, Is.EqualTo (new Version (16, 2)), "XcodeVersion should be populated."); + Assert.That (locator.DTXcode, Is.EqualTo ("1620"), "DTXcode should be populated."); + Assert.That (locator.XcodeLocation, Is.EqualTo (fakeXcode), "XcodeLocation should be set."); + } finally { + Directory.Delete (fakeXcode, recursive: true); + } + } + + [Test] + public void TryLocatingXcode_Override_WithContentsDeveloper_PopulatesXcodeVersion () + { + var fakeXcode = CreateFakeXcodeBundle (version: "26.2", dtXcode: "2620"); + try { + var locator = new XcodeLocator (ConsoleLogger.Instance); + var pathWithDeveloper = Path.Combine (fakeXcode, "Contents", "Developer"); + Directory.CreateDirectory (pathWithDeveloper); + var found = locator.TryLocatingXcode (pathWithDeveloper); + Assert.That (found, Is.True, "TryLocatingXcode should return true when path includes /Contents/Developer."); + Assert.That (locator.XcodeVersion, Is.EqualTo (new Version (26, 2)), "XcodeVersion should be populated."); + Assert.That (locator.DTXcode, Is.EqualTo ("2620"), "DTXcode should be populated."); + Assert.That (locator.XcodeLocation, Is.EqualTo (fakeXcode), "XcodeLocation should be canonicalized (no /Contents/Developer suffix)."); + } finally { + Directory.Delete (fakeXcode, recursive: true); + } + } + + [Test] + public void TryLocatingXcode_NullOverride_ReturnsFalseWhenNothingFound () + { + // With SupportEnvironmentVariableLookup and SupportSettingsFileLookup both false + // and no xcode-select available (Linux CI), TryLocatingXcode should return false. + var locator = new XcodeLocator (ConsoleLogger.Instance) { + SupportEnvironmentVariableLookup = false, + SupportSettingsFileLookup = false, + }; + // On Linux, xcode-select doesn't exist so TryGetSystemXcode returns false. + // We just verify it doesn't throw. + Assert.DoesNotThrow (() => locator.TryLocatingXcode (null)); + } + + [Test] + public void TryLocatingXcode_Override_MissingVersionPlist_IgnoresBrokenPath () + { + var dir = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName () + ".app"); + var contentsDir = Path.Combine (dir, "Contents"); + Directory.CreateDirectory (contentsDir); + // No version.plist created. + File.WriteAllText (Path.Combine (contentsDir, "Info.plist"), @" + +DTXcode1620 +"); + try { + var locator = new XcodeLocator (ConsoleLogger.Instance); + locator.TryLocatingXcode (dir); + // The broken path must never be accepted as the Xcode location, + // even if a fallback (xcode-select) finds a different valid Xcode. + Assert.That (locator.XcodeLocation, Is.Not.EqualTo (dir), "A bundle missing version.plist should not be set as XcodeLocation."); + } finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void TryLocatingXcode_Override_MissingInfoPlist_IgnoresBrokenPath () + { + var dir = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName () + ".app"); + var contentsDir = Path.Combine (dir, "Contents"); + Directory.CreateDirectory (contentsDir); + File.WriteAllText (Path.Combine (contentsDir, "version.plist"), @" + +CFBundleShortVersionString16.2CFBundleVersion16C5032a +"); + // No Info.plist created. + try { + var locator = new XcodeLocator (ConsoleLogger.Instance); + locator.TryLocatingXcode (dir); + // The broken path must never be accepted as the Xcode location, + // even if a fallback (xcode-select) finds a different valid Xcode. + Assert.That (locator.XcodeLocation, Is.Not.EqualTo (dir), "A bundle missing Info.plist should not be set as XcodeLocation."); + } finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void TryGetSystemXcode_WhenXcodeSelectMissing_ReturnsFalse () + { + // On Linux (CI), /usr/bin/xcode-select doesn't exist, so TryGetSystemXcode returns false. + if (File.Exists ("/usr/bin/xcode-select")) + Assert.Ignore ("This test only applies when xcode-select is not present."); + + var result = XcodeLocator.TryGetSystemXcode (ConsoleLogger.Instance, out var path); + Assert.That (result, Is.False, "TryGetSystemXcode should return false when xcode-select is not installed."); + Assert.That (path, Is.Null, "path should be null when xcode-select is not installed."); + } + } +}