Skip to content

Commit 5c74beb

Browse files
[tests] Cover AppCompat Java alias casts (#12609)
## Summary - add a focused `MSBuildDeviceIntegration` AppCompat app for Java alias/cast parity - inflate real `Toolbar` and `AppCompatImageButton` views from XML, then verify most-derived binding selection and repeated peer identity - cover `JavaCast<T>` and `JavaAs<T>` for a concrete same-JNI-name alias, an AppCompat Java interface, managed-created peers, and a caller-directed closed generic `JavaList<string>` wrapper - run the same Release fixture with llvm-ir/CoreCLR, trimmable/CoreCLR, and trimmable/NativeAOT No production change is included: the unchanged focused fixture is green in all three configurations. ## Investigation The existing NativeAOT skips all record the same historical startup failure: - `InstallAndRunTests.CheckXamarinFormsAppDeploysAndAButtonWorks` - `BundleToolNoAbiSplitTests.InstallAndRun` - `DebuggingTest.ApplicationRunsWithoutDebugger` The reported exception converts `AndroidX.AppCompat.Widget.AppCompatImageButton` to `AndroidX.AppCompat.Widget.Toolbar` inside `Xamarin.Forms.Platform.Android.FormsAppCompatActivity.OnCreate`. The BundleTool/Debugging skips were introduced by `b58d4606e6` (#10632); the InstallAndRun skip was added when that test gained NativeAOT coverage in `847c5fe161` (#10635). The mismatch is stale/not reproducible for the current AppCompat binding and focused view-inflation path. The three broad Xamarin.Forms 5.0 skips remain unchanged: that exact legacy package is not present in the required offline package cache, and those tests also combine unrelated Forms, bundle-tool, and debugger behavior. They remain separate follow-up validation rather than being removed without executing the original fixtures. ## Artifact evidence For trimmable NativeAOT: - `_UnnamedProject.TypeMap.dll` contains an `AppCompatImageButton` alias holder with `[0]` targeting `AndroidX.AppCompat.Widget.AppCompatImageButton` and `[1]` targeting `UnnamedProject.AppCompatImageButtonAlias` - `acw-map.txt` maps both managed types to `androidx.appcompat.widget.AppCompatImageButton` - DEX contains the real `androidx/appcompat/widget/AppCompatImageButton`, `androidx/appcompat/widget/Toolbar`, and generated `MainActivity` - startup logs contain `APPCOMPAT_ALIAS_CASTS_PASS` Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9608c4d commit 5c74beb

1 file changed

Lines changed: 273 additions & 0 deletions

File tree

tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,279 @@ public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet att
17511751
Assert.IsTrue (didStart, "Activity should have started.");
17521752
}
17531753

1754+
[TestCase ("llvm-ir", AndroidRuntime.CoreCLR)]
1755+
[TestCase ("trimmable", AndroidRuntime.CoreCLR)]
1756+
[TestCase ("trimmable", AndroidRuntime.NativeAOT)]
1757+
public void AppCompatJavaAliasCastsAndInflation (
1758+
string typemapImplementation,
1759+
AndroidRuntime runtime)
1760+
{
1761+
const string expectedLogcatOutput = "APPCOMPAT_ALIAS_CASTS_PASS";
1762+
1763+
if (IgnoreUnsupportedConfiguration (runtime, release: true)) {
1764+
return;
1765+
}
1766+
1767+
var packageSuffix = $"appcompataliascasts{typemapImplementation.Replace ("-", "")}";
1768+
var packageName = PackageUtils.MakePackageName (runtime, packageSuffix);
1769+
var proj = new XamarinAndroidApplicationProject (
1770+
packageName: packageName) {
1771+
IsRelease = true,
1772+
};
1773+
proj.SetRuntime (runtime);
1774+
proj.SetRuntimeIdentifiers (new [] { DeviceAbi });
1775+
proj.SetProperty ("AndroidTypeMapImplementation", typemapImplementation);
1776+
proj.SetDefaultTargetDevice ();
1777+
proj.PackageReferences.Add (new Package {
1778+
Id = "Xamarin.AndroidX.AppCompat",
1779+
Version = "1.7.1.3",
1780+
});
1781+
proj.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styles.xml") {
1782+
TextContent = () => """
1783+
<?xml version="1.0" encoding="utf-8"?>
1784+
<resources>
1785+
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar" />
1786+
</resources>
1787+
""",
1788+
});
1789+
proj.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\layout\\alias_casts.xml") {
1790+
TextContent = () => """
1791+
<?xml version="1.0" encoding="utf-8"?>
1792+
<LinearLayout
1793+
xmlns:android="http://schemas.android.com/apk/res/android"
1794+
android:layout_width="match_parent"
1795+
android:layout_height="match_parent"
1796+
android:orientation="vertical">
1797+
<androidx.appcompat.widget.Toolbar
1798+
android:id="@+id/toolbar"
1799+
android:layout_width="match_parent"
1800+
android:layout_height="wrap_content" />
1801+
<androidx.appcompat.widget.AppCompatImageButton
1802+
android:id="@+id/image_button"
1803+
android:layout_width="wrap_content"
1804+
android:layout_height="wrap_content"
1805+
android:src="@android:drawable/ic_menu_add" />
1806+
</LinearLayout>
1807+
""",
1808+
});
1809+
proj.MainActivity = """
1810+
using System;
1811+
1812+
using Android.App;
1813+
using Android.Content;
1814+
using Android.OS;
1815+
using Android.Runtime;
1816+
using Android.Util;
1817+
using Android.Views;
1818+
1819+
using AndroidX.AppCompat.App;
1820+
using AndroidX.AppCompat.Widget;
1821+
using AndroidX.Core.View;
1822+
1823+
using Java.Interop;
1824+
1825+
namespace UnnamedProject
1826+
{
1827+
[Activity (
1828+
Label = "AppCompat alias casts",
1829+
MainLauncher = true,
1830+
Theme = "@style/AppTheme")]
1831+
public class MainActivity : AppCompatActivity
1832+
{
1833+
public MainActivity ()
1834+
{
1835+
}
1836+
1837+
protected MainActivity (IntPtr handle, JniHandleOwnership transfer)
1838+
: base (handle, transfer)
1839+
{
1840+
}
1841+
1842+
protected override void OnCreate (Bundle savedInstanceState)
1843+
{
1844+
base.OnCreate (savedInstanceState);
1845+
SetContentView (Resource.Layout.alias_casts);
1846+
1847+
VerifyInflatedViews ();
1848+
VerifyManagedCreatedViews ();
1849+
VerifyCallerDirectedGenericWrapper ();
1850+
1851+
Log.Info ("JavaAliasCasts", "APPCOMPAT_ALIAS_CASTS_PASS");
1852+
}
1853+
1854+
void VerifyInflatedViews ()
1855+
{
1856+
const bool allowRegisteredAliasInflation = __ALLOW_REGISTERED_ALIAS_INFLATION__;
1857+
1858+
var toolbarView = FindViewById (Resource.Id.toolbar);
1859+
Require (toolbarView != null, "Inflated Toolbar was not found.");
1860+
Require (
1861+
toolbarView.GetType () == typeof (AndroidX.AppCompat.Widget.Toolbar),
1862+
$"Expected most-derived Toolbar binding; {Describe (toolbarView)}.");
1863+
1864+
var toolbar = JavaObjectExtensions.JavaCast<AndroidX.AppCompat.Widget.Toolbar> (toolbarView);
1865+
var toolbarAs = JavaPeerableExtensions.JavaAs<AndroidX.AppCompat.Widget.Toolbar> (toolbarView);
1866+
Require (ReferenceEquals (toolbarView, toolbar), "JavaCast<Toolbar> did not preserve the existing peer.");
1867+
Require (ReferenceEquals (toolbarView, toolbarAs), "JavaAs<Toolbar> did not preserve the existing peer.");
1868+
Require (
1869+
ReferenceEquals (toolbarView, FindViewById (Resource.Id.toolbar)),
1870+
"Repeated Toolbar lookup did not preserve peer identity.");
1871+
1872+
var imageView = FindViewById (Resource.Id.image_button);
1873+
Require (imageView != null, "Inflated AppCompatImageButton was not found.");
1874+
Require (
1875+
imageView.GetType () == typeof (AppCompatImageButton) ||
1876+
(allowRegisteredAliasInflation && imageView.GetType () == typeof (AppCompatImageButtonAlias)),
1877+
$"Expected the canonical AppCompatImageButton binding; {Describe (imageView)}.");
1878+
Require (
1879+
ReferenceEquals (imageView, FindViewById (Resource.Id.image_button)),
1880+
"Repeated AppCompatImageButton lookup did not preserve peer identity.");
1881+
1882+
using var untypedImage = new Java.Lang.Object (
1883+
imageView.Handle,
1884+
JniHandleOwnership.DoNotTransfer | JniHandleOwnership.DoNotRegister);
1885+
var alias = JavaObjectExtensions.JavaCast<AppCompatImageButtonAlias> (untypedImage);
1886+
try {
1887+
using var aliasAs = JavaPeerableExtensions.JavaAs<AppCompatImageButtonAlias> (untypedImage);
1888+
Require (alias != null, "JavaCast did not select the concrete AppCompatImageButton alias.");
1889+
Require (aliasAs != null, "JavaAs did not select the concrete AppCompatImageButton alias.");
1890+
Require (
1891+
JNIEnv.IsSameObject (imageView.Handle, alias.Handle),
1892+
"Concrete alias did not retain the inflated Java object.");
1893+
Require (
1894+
JNIEnv.IsSameObject (imageView.Handle, aliasAs.Handle),
1895+
"Concrete alias JavaAs did not retain the inflated Java object.");
1896+
if (imageView is AppCompatImageButtonAlias) {
1897+
Require (ReferenceEquals (imageView, alias), "JavaCast did not preserve the inflated alias peer.");
1898+
}
1899+
Require (
1900+
AppCompatImageButtonAlias.HandleConstructorCalls == 2,
1901+
"Inflation and concrete alias casts did not use the expected handle constructors.");
1902+
1903+
var tintable = JavaObjectExtensions.JavaCast<ITintableBackgroundView> (untypedImage);
1904+
try {
1905+
using var tintableAs = JavaPeerableExtensions.JavaAs<ITintableBackgroundView> (untypedImage);
1906+
Require (tintable != null, "JavaCast did not resolve the AppCompat interface.");
1907+
Require (tintableAs != null, "JavaAs did not resolve the AppCompat interface.");
1908+
Require (
1909+
JNIEnv.IsSameObject (imageView.Handle, tintable.Handle),
1910+
"Interface cast did not retain the inflated Java object.");
1911+
Require (
1912+
JNIEnv.IsSameObject (imageView.Handle, tintableAs.PeerReference.Handle),
1913+
"Interface JavaAs did not retain the inflated Java object.");
1914+
} finally {
1915+
if (tintable != null && !ReferenceEquals (tintable, imageView)) {
1916+
tintable.Dispose ();
1917+
}
1918+
}
1919+
} finally {
1920+
if (alias != null && !ReferenceEquals (alias, imageView)) {
1921+
alias.Dispose ();
1922+
}
1923+
}
1924+
}
1925+
1926+
void VerifyManagedCreatedViews ()
1927+
{
1928+
using var image = new AppCompatImageButton (this);
1929+
using var untypedImage = new Java.Lang.Object (
1930+
image.Handle,
1931+
JniHandleOwnership.DoNotTransfer | JniHandleOwnership.DoNotRegister);
1932+
var castImage = JavaObjectExtensions.JavaCast<AppCompatImageButton> (untypedImage);
1933+
var imageAs = JavaPeerableExtensions.JavaAs<AppCompatImageButton> (image);
1934+
Require (ReferenceEquals (image, castImage), "Managed-created JavaCast did not preserve peer identity.");
1935+
Require (ReferenceEquals (image, imageAs), "Managed-created JavaAs did not preserve peer identity.");
1936+
1937+
using var alias = new AppCompatImageButtonAlias (this);
1938+
using var untypedAlias = new Java.Lang.Object (
1939+
alias.Handle,
1940+
JniHandleOwnership.DoNotTransfer | JniHandleOwnership.DoNotRegister);
1941+
var castAlias = JavaObjectExtensions.JavaCast<AppCompatImageButtonAlias> (untypedAlias);
1942+
var aliasAs = JavaPeerableExtensions.JavaAs<AppCompatImageButtonAlias> (alias);
1943+
Require (ReferenceEquals (alias, castAlias), "Managed-created alias JavaCast did not preserve peer identity.");
1944+
Require (ReferenceEquals (alias, aliasAs), "Managed-created alias JavaAs did not preserve peer identity.");
1945+
}
1946+
1947+
static void VerifyCallerDirectedGenericWrapper ()
1948+
{
1949+
using var list = new JavaList ();
1950+
using var untyped = new Java.Lang.Object (
1951+
list.Handle,
1952+
JniHandleOwnership.DoNotTransfer | JniHandleOwnership.DoNotRegister);
1953+
using var generic = JavaObjectExtensions.JavaCast<JavaList<string>> (untyped);
1954+
using var genericAs = JavaPeerableExtensions.JavaAs<JavaList<string>> (untyped);
1955+
Require (generic != null, "JavaCast did not create the caller-directed generic wrapper.");
1956+
Require (genericAs != null, "JavaAs did not create the caller-directed generic wrapper.");
1957+
Require (
1958+
JNIEnv.IsSameObject (generic.Handle, genericAs.Handle),
1959+
"Caller-directed generic wrappers did not retain the same Java object.");
1960+
generic.Add ("alias");
1961+
Require (genericAs [0] == "alias", "Caller-directed generic wrappers did not round trip their value.");
1962+
Require (
1963+
ReferenceEquals (generic, JavaPeerableExtensions.JavaAs<JavaList<string>> (generic)),
1964+
"Generic JavaAs did not preserve the typed peer.");
1965+
}
1966+
1967+
static string Describe (Java.Lang.Object value)
1968+
{
1969+
return $"managed={value.GetType ().FullName}, java={JNIEnv.GetClassNameFromInstance (value.Handle)}";
1970+
}
1971+
1972+
static void Require (bool condition, string message)
1973+
{
1974+
if (!condition) {
1975+
throw new InvalidOperationException (message);
1976+
}
1977+
}
1978+
}
1979+
1980+
[Register ("androidx/appcompat/widget/AppCompatImageButton", DoNotGenerateAcw = true)]
1981+
public sealed class AppCompatImageButtonAlias : AppCompatImageButton
1982+
{
1983+
public static int HandleConstructorCalls;
1984+
1985+
public AppCompatImageButtonAlias (Context context)
1986+
: base (context)
1987+
{
1988+
}
1989+
1990+
public AppCompatImageButtonAlias (Context context, IAttributeSet attrs)
1991+
: base (context, attrs)
1992+
{
1993+
}
1994+
1995+
public AppCompatImageButtonAlias (Context context, IAttributeSet attrs, int style)
1996+
: base (context, attrs, style)
1997+
{
1998+
}
1999+
2000+
public AppCompatImageButtonAlias (IntPtr handle, JniHandleOwnership transfer)
2001+
: base (handle, transfer)
2002+
{
2003+
HandleConstructorCalls++;
2004+
}
2005+
}
2006+
}
2007+
""".Replace (
2008+
"__ALLOW_REGISTERED_ALIAS_INFLATION__",
2009+
typemapImplementation == "llvm-ir" ? "true" : "false");
2010+
using var builder = CreateApkBuilder (packageName: packageName);
2011+
Assert.AreEqual (proj.PackageName, TestPackageNames [packageName], "Teardown should track the installed package.");
2012+
RunAdbCommand ($"uninstall {proj.PackageName}");
2013+
try {
2014+
Assert.True (builder.Install (proj), "Project should have installed.");
2015+
ClearAdbLogcat ();
2016+
RunProjectAndAssert (proj, builder, doNotCleanupOnUpdate: true);
2017+
Assert.True (WaitForActivityToStart (proj.PackageName, "MainActivity",
2018+
Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), ActivityStartTimeoutInSeconds), "Activity should have started.");
2019+
Assert.True (MonitorAdbLogcat (line => line.Contains (expectedLogcatOutput),
2020+
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"), 45), $"Output did not contain {expectedLogcatOutput}.");
2021+
} finally {
2022+
RunAdbCommand ($"shell am force-stop --user all {proj.PackageName}");
2023+
RunAdbCommand ($"uninstall {proj.PackageName}");
2024+
}
2025+
}
2026+
17542027
[Test]
17552028
public void CheckXamarinFormsAppDeploysAndAButtonWorks ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)
17562029
{

0 commit comments

Comments
 (0)