Skip to content

[browser] More Wasm.Build.Tests on CoreCLR#127281

Draft
maraf wants to merge 10 commits intodotnet:mainfrom
maraf:maraf/wbt-coreclr-enable-non-aot-tests
Draft

[browser] More Wasm.Build.Tests on CoreCLR#127281
maraf wants to merge 10 commits intodotnet:mainfrom
maraf:maraf/wbt-coreclr-enable-non-aot-tests

Conversation

@maraf
Copy link
Copy Markdown
Member

@maraf maraf commented Apr 22, 2026

Note

This PR description was AI-generated (GitHub Copilot CLI).

Follow-up to #127073. Now that WASM templates install and native relink work on CoreCLR in Wasm.Build.Tests (WBT), enable the non-AOT tests that were previously excluded by the class-level [TestCategory("native")] trait filter.

What changes

src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj excludes category=native on CoreCLR:

<_XUnitTraitArg Condition="'$(RuntimeFlavor)' == 'CoreCLR'">-notrait category=native -notrait category=mono -notrait category=workload</_XUnitTraitArg>

xunit merges class-level and method-level traits, so moving native to a sub-category (e.g. native-coreclr) at method level does not help — the class-level native still causes exclusion. The fix is to remove the class-level tag and re-apply native only on the AOT-only methods.

File Change
NativeBuildTests.cs Remove class tag. Method-tag AOTNotSupportedWithNoTrimming, IntermediateBitcodeToObjectFilesAreNotLLVMIR, NativeBuildIsRequired (all AOT-only). Non-AOT methods (SimpleNativeBuild, ZipArchiveInteropTest) now run on CoreCLR.
DllImportTests.cs Remove class tag. All methods are non-AOT — all run on CoreCLR.
PInvokeTableGeneratorTests.cs Remove class tag. Method-tag EnsureWasmAbiRulesAreFollowedInAOT, EnsureComInteropCompilesInAOT.
NativeLibraryTests.cs Remove class tag. Split the four theories that had both [BuildAndRun(aot:false)] and [BuildAndRun(aot:true,…)] into a non-AOT method and an _AOT method tagged native (they delegate to a shared …Core helper). The pre-existing [ActiveIssue(#103566)] on ProjectUsingSkiaSharp is applied to both halves.

Splitting (rather than runtime-skip) keeps test reporting honest: AOT-on-CoreCLR shows up as "excluded by filter" instead of a false pass.

Not in scope: other [TestCategory("native")] classes in WBT (BuildPublishTests, NativeRebuildTests/*, WasmSIMDTests, IcuTests, SatelliteAssembliesTests, …). Those often encode reasons beyond "class-level native is too broad" and are left for a follow-up.

Validation

  • Local build: ./dotnet.sh build src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj -c Release /p:TargetOS=browser /p:RuntimeFlavor=CoreCLR /p:Scenario=BuildWasmApps → 0W/0E.
  • Helix end-to-end validation: this PR's CI.

Copilot AI review requested due to automatic review settings April 22, 2026 15:30

This comment was marked as resolved.

@maraf maraf added arch-wasm WebAssembly architecture test-enhancement Improvements of test source code os-browser Browser variant of arch-wasm labels Apr 22, 2026
@maraf maraf added this to the 11.0.0 milestone Apr 22, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

Remove class-level [TestCategory("native")] from NativeBuildTests,
NativeLibraryTests, PInvokeTableGeneratorTests, and DllImportTests so
non-AOT methods pass the CoreCLR xunit filter (-notrait category=native).
AOT-only methods get a method-level [TestCategory("native")] to keep
them excluded on CoreCLR.

NativeLibraryTests: four mixed theories (both aot=false and aot=true
inline data) are split into a non-AOT theory and a separate AOT-only
theory tagged 'native', because xunit trait filtering is per-method,
not per-theory-row.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 24, 2026 08:08
@maraf maraf force-pushed the maraf/wbt-coreclr-enable-non-aot-tests branch from 07aa682 to 8debc4a Compare April 24, 2026 08:08

This comment was marked as resolved.

Copilot AI and others added 2 commits April 24, 2026 10:30
Change [TestCategory("native")] to [TestCategory("native-coreclr")] for tests
that use native relink but should also run on CoreCLR:

- IcuShardingTests: CustomIcuShard, AutomaticShardSelectionDependingOnEnvLocale
- IcuShardingTests2: DefaultAvailableIcuShardsFromRuntimePack
- IcuTests: FullIcuFromRuntimePackWithInvariant, FullIcuFromRuntimePackWithCustomIcu
- InvariantGlobalizationTests: RelinkingWithoutAOT
- MemoryTests: entire class (AllocateLargeHeapThenRepeatedlyInterop)

CoreCLR always has native relink support, so these tests can run on both
Mono and CoreCLR. The CoreCLR xunit filter excludes category=native but
not category=native-coreclr.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Without these entries in BuildWasmAppsJobsListCoreCLR.txt, the test
classes whose class-level [TestCategory("native")] was removed (or
retagged to native-coreclr) in this PR would still not be scheduled
as Helix work items for the CoreCLR BuildWasmApps scenario.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 24, 2026 10:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Comment on lines 54 to 57
[Theory]
[MemberData(nameof(FullIcuWithInvariantTestData), parameters: new object[] { Configuration.Release })]
[TestCategory("native")]
[TestCategory("native-coreclr")]
public async Task FullIcuFromRuntimePackWithInvariant(Configuration config=Configuration.Release, bool aot=false, bool invariant=true, bool fullIcu=true, string testedLocales="Array.Empty<Locale>()") =>
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing these theories to TestCategory("native-coreclr") makes them eligible to run under the CoreCLR lane filter (which only excludes category=native). However, the test data includes aot=true cases (boolOptions), and CoreCLR builds set <UseMonoRuntime>false</UseMonoRuntime>; AOT compilation is likely unsupported and will attempt to run anyway. Consider splitting into non-AOT methods (tagged native-coreclr) and AOT methods (keep category=native so they remain excluded on CoreCLR), or adjust the MemberData to exclude aot=true when BuildTestBase.IsCoreClrRuntime.

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 45
[Theory]
[MemberData(nameof(IcuExpectedAndMissingCustomShardTestData), parameters: new object[] { Configuration.Release })]
[TestCategory("native")]
[TestCategory("native-coreclr")]
public async Task CustomIcuShard(Configuration config, bool aot, string customIcuPath, string customLocales, bool onlyPredefinedCultures) =>
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests use boolOptions for the aot parameter (so they generate AOT and non-AOT variants). Retagging them to native-coreclr will cause the AOT variants to run on the CoreCLR lane (since only category=native is excluded), which is likely unsupported for CoreCLR (UseMonoRuntime=false). Split/tag AOT variants as category=native (excluded on CoreCLR) or filter aot=true out for CoreCLR runs.

Copilot uses AI. Check for mistakes.
Comment on lines 39 to 42
[Theory]
[MemberData(nameof(IcuExpectedAndMissingShardFromRuntimePackTestData), parameters: new object[] { Configuration.Release })]
[TestCategory("native")]
[TestCategory("native-coreclr")]
public async Task DefaultAvailableIcuShardsFromRuntimePack(Configuration config, bool aot, string shardName, string testedLocales) =>
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This theory generates both aot=false and aot=true cases via boolOptions. After changing the category to native-coreclr, the AOT variants will no longer be excluded by the CoreCLR trait filter (-notrait category=native) and may run on CoreCLR where AOT is likely unsupported. Consider splitting/tagging AOT variants as category=native (excluded) or changing the data source to omit aot=true on CoreCLR.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to 17
[TestCategory("native-coreclr")]
public class MemoryTests : WasmTemplateTestsBase
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the class retagged to native-coreclr, this test will start running in the CoreCLR lane. The build assertion currently uses ExpectSuccess: BuildTestBase.IsUsingWorkloads, but CoreCLR WBT runs with TestUsingWorkloads=false while native relink is now supported (emsdk is provisioned separately). That combination likely makes the test fail (it will expect the native build to fail even if it succeeds). Consider basing ExpectSuccess/execution on whether native builds are actually available (e.g., IsUsingWorkloads || IsCoreClrRuntime), or keep the test excluded until its expectations are updated.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
Wasm.Build.Tests.IcuShardingTests
Wasm.Build.Tests.IcuShardingTests2
Wasm.Build.Tests.IcuTests
Wasm.Build.Tests.InvariantGlobalizationTests
Wasm.Build.Tests.MemoryTests
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says additional TestCategory("native") classes like IcuTests are "not in scope" and left for follow-up, but this change adds Icu*, InvariantGlobalizationTests, and MemoryTests to the CoreCLR job list. Either update the PR description to reflect the expanded scope or remove these additions to match the stated intent.

Copilot uses AI. Check for mistakes.
maraf and others added 2 commits April 24, 2026 12:41
EnsureWasmTemplatesInstalled placed .dotnet-cli-home next to the
template nupkg (which on Linux Helix agents is under the read-only
correlation payload), causing 'Read-only file system' / 'Access denied'
errors for every test class that calls CreateWasmTemplateProject.

Always use BuildEnvironment.TmpPath — the harness's writable scratch
dir — instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EnsureWasmTemplatesInstalled set DOTNET_CLI_HOME to TmpPath/.dotnet-cli-home
when running 'dotnet new install'. The subsequent 'dotnet new <template>'
call goes through DotNetCommand(useDefaultArgs: false), which does not
apply BuildEnvironment.EnvVars to the child process, so it inherited the
Helix-set DOTNET_CLI_HOME (e.g. /root/helix/work/workitem/e/.dotnet).

Two different DOTNET_CLI_HOME values mean two different template caches:
the install lands in one, the lookup misses in the other, producing
'No templates or subcommands found matching: wasmbrowser' (exit 103) on
every test class that calls CreateWasmTemplateProject.

Prefer the inherited DOTNET_CLI_HOME so install and lookup share a cache;
fall back to TmpPath only when not set (preserves the read-only-FS fix
for environments without an inherited value).

Reproduced locally with DOTNET_CLI_HOME=/tmp/helix-cli-home; templates
were installed under wbt artifacts/.dotnet-cli-home but 'dotnet new'
read from /tmp/helix-cli-home. After the fix, both paths share
/tmp/helix-cli-home and 'dotnet new wasmbrowser' succeeds.

Also adds [diag] Console.WriteLine entries logging both the install-time
and invocation-time DOTNET_CLI_HOME so Helix work-item console logs make
similar future regressions trivial to diagnose.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 08:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs:104

  • EnsureWasmTemplatesInstalled sets DOTNET_CLI_HOME only for the dotnet new install process. The subsequent dotnet new <template> invocation (via DotNetCommand(..., useDefaultArgs:false)) does not set DOTNET_CLI_HOME, so it may look in a different template cache when DOTNET_CLI_HOME is not already set in the test process environment (e.g., local runs). To make this robust, propagate the chosen dotnetCliHome to the dotnet new <template> command (or set it on the current process via Environment.SetEnvironmentVariable before invoking dotnet new).
        // [diag] Log DOTNET_CLI_HOME inherited by the `dotnet new <template>` invocation.
        // The template engine reads installed templates from this location; if it differs
        // from the DOTNET_CLI_HOME used by EnsureWasmTemplatesInstalled, the template
        // won't be found.
        string inheritedCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME") ?? "<unset>";
        string envVarsCliHome = s_buildEnv.EnvVars.TryGetValue("DOTNET_CLI_HOME", out string? evCliHome) ? evCliHome : "<unset-in-EnvVars>";
        Console.WriteLine($"[diag] DOTNET_CLI_HOME inherited by `dotnet new {template.ToString().ToLower()}`: '{inheritedCliHome}' (buildEnv.EnvVars: '{envVarsCliHome}')");

        using DotNetCommand cmd = new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false);
        CommandResult result = cmd.WithWorkingDirectory(_projectDir)
            .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
            .ExecuteWithCapturedOutput($"new {template.ToString().ToLower()} {extraArgs}")
            .EnsureSuccessful();

maraf and others added 2 commits April 27, 2026 10:26
- Inline callhelpers.hpp / minipal/entrypoints.h declarations into the
  pre-included coreclr_compat.h and stop emitting #include lines for
  those headers from the CoreCLR generators, so pinvoke-table.cpp and
  wasm_m2n_invoke.g.cpp compile on Helix where the in-repo header paths
  do not exist.
- Make BuildEnvironment.GetRuntimePackDir flavor-aware: on CoreCLR the
  runtime pack is Microsoft.NETCore.App.Runtime.<rid> (no .Mono. segment).
- Mark DllImportTests.NativeLibraryWithVariadicFunctions as Mono-only;
  CoreCLR's PInvoke generator does not emit the expected varargs warning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….targets

The previous attempt to generate coreclr_compat.h from a _CompatHeaderLines
ItemGroup hit an MSBuild item-spec dedup/parsing issue: items whose Include
value contained both `{...}` braces and embedded `%3B` semicolons were
silently dropped from the output (verified with a minimal repro). That left
the generated header with missing `StringToWasmSigThunk` and
`ReverseThunkMapEntry` struct declarations, so pinvoke-table.cpp /
wasm_m2n_invoke.g.cpp failed to compile on Helix.

Replace the inline ItemGroup generation with a static coreclr_compat.h next
to BrowserWasmApp.CoreCLR.targets and `-include` it via
$(MSBuildThisFileDirectory). The file ships into the Helix correlation
payload via the existing HelixCorrelationPayload Include="$(BrowserBuildTargetsDir)"
mapping (sendtohelix-browser.targets), so it lands at
correlation/build/wasm/coreclr_compat.h alongside the targets file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 11:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Comment on lines +21 to +28
// CoreCLR type stubs
#ifndef _CORECLR_COMPAT_TYPES
#define _CORECLR_COMPAT_TYPES
typedef void MethodDesc;
typedef uintptr_t PCODE;
typedef uint32_t ULONG;
#define INTERP_STACK_SLOT_SIZE 8u
#endif
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coreclr_compat.h is intended to replace <minipal/utils.h>, but it doesn’t currently define NOINLINE. The generated wasm_m2n_invoke.g.cpp uses NOINLINE for portable-entrypoint thunks, so compilation will fail unless this macro is provided (or the generator includes the proper header again).

Copilot uses AI. Check for mistakes.
// from the DOTNET_CLI_HOME used by EnsureWasmTemplatesInstalled, the template
// won't be found.
string inheritedCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME") ?? "<unset>";
string envVarsCliHome = s_buildEnv.EnvVars.TryGetValue("DOTNET_CLI_HOME", out string? evCliHome) ? evCliHome : "<unset-in-EnvVars>";
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnvVars is an IDictionary<string, string> (non-null values), but this uses out string? evCliHome and assigns it to a non-null string. With #nullable enable, this is likely to introduce a nullable warning (and may fail the build if warnings are treated as errors). Use out string evCliHome (or null-coalesce) to keep nullability consistent.

Suggested change
string envVarsCliHome = s_buildEnv.EnvVars.TryGetValue("DOTNET_CLI_HOME", out string? evCliHome) ? evCliHome : "<unset-in-EnvVars>";
string envVarsCliHome = s_buildEnv.EnvVars.TryGetValue("DOTNET_CLI_HOME", out string evCliHome) ? evCliHome : "<unset-in-EnvVars>";

Copilot uses AI. Check for mistakes.
Comment on lines +227 to 236
// writable workitem path); otherwise fall back to TmpPath. Aligning with
// the inherited value ensures `dotnet new install` and the subsequent
// `dotnet new <template>` invocation share the same template cache —
// those `dotnet new <template>` calls (via DotNetCommand with
// useDefaultArgs:false) inherit DOTNET_CLI_HOME from the test process.
string? inheritedCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME");
string dotnetCliHome = !string.IsNullOrWhiteSpace(inheritedCliHome)
? inheritedCliHome
: Path.Combine(BuildEnvironment.TmpPath, ".dotnet-cli-home");
Directory.CreateDirectory(dotnetCliHome);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When DOTNET_CLI_HOME is not set in the test process environment, EnsureWasmTemplatesInstalled installs templates into BuildEnvironment.TmpPath via psi.Environment["DOTNET_CLI_HOME"], but the later dotnet new <template> invocation does not set DOTNET_CLI_HOME (and DotNetCommand is created with useDefaultArgs:false, so it won’t apply s_buildEnv.EnvVars). This makes the fallback path inconsistent and can cause templates to not be found on local runs. Consider setting DOTNET_CLI_HOME for the current process and/or passing it explicitly into the dotnet new command as well.

Suggested change
// writable workitem path); otherwise fall back to TmpPath. Aligning with
// the inherited value ensures `dotnet new install` and the subsequent
// `dotnet new <template>` invocation share the same template cache —
// those `dotnet new <template>` calls (via DotNetCommand with
// useDefaultArgs:false) inherit DOTNET_CLI_HOME from the test process.
string? inheritedCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME");
string dotnetCliHome = !string.IsNullOrWhiteSpace(inheritedCliHome)
? inheritedCliHome
: Path.Combine(BuildEnvironment.TmpPath, ".dotnet-cli-home");
Directory.CreateDirectory(dotnetCliHome);
// writable workitem path); otherwise fall back to TmpPath. Persist the
// chosen value into the current test process as well so subsequent
// `dotnet new <template>` invocations (via DotNetCommand with
// useDefaultArgs:false) inherit the same template cache location.
string? inheritedCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME");
string dotnetCliHome = !string.IsNullOrWhiteSpace(inheritedCliHome)
? inheritedCliHome
: Path.Combine(BuildEnvironment.TmpPath, ".dotnet-cli-home");
Directory.CreateDirectory(dotnetCliHome);
Environment.SetEnvironmentVariable("DOTNET_CLI_HOME", dotnetCliHome);

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +278
<!-- All declarations needed by ManagedToNativeGenerator output (pinvoke-table.cpp,
wasm_m2n_invoke.g.cpp) are provided by coreclr_compat.h, which ships next to this
targets file. This avoids relying on the in-repo coreclr/vm/wasm and
native/minipal headers, which are not present in the WBT Helix payload. -->
<_EmccCFlags Include="-include &quot;$(MSBuildThisFileDirectory)coreclr_compat.h&quot;" />
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_CoreCLRGenerateManagedToNative still adds Dependencies="$(_WasmCompileRsp);$(_WasmIntermediateOutputPath)coreclr_compat.h" for the generated pinvoke/interp sources later in the file, but this PR removes the intermediate coreclr_compat.h generation and switches to $(MSBuildThisFileDirectory)coreclr_compat.h. Update those remaining Dependencies values to match the new header location (or drop the dependency) to keep incremental builds correct.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

maraf commented Apr 29, 2026

Note

This comment was generated with help from GitHub Copilot.

cc @pavelsavara

Local repro of the IcuTests/InvariantGlobalizationTests failures (Group B)

I rebuilt the CoreCLR-wasm runtime fresh from this PR's HEAD and reproduced the failure locally — same surface error as CI:

[error] DOTNET: Unhandled error: [object WebAssembly.Exception]
    at R (.../_framework/dotnet.diagnostics.*.js:4:4715)
    at A (.../_framework/dotnet.diagnostics.*.js:4:4592)

I then took the published app, served it with xharness wasm webserver, and connected Playwright with forwardConsole: true + withDiagnosticTracing(true), which surfaced the actual managed-side message:

[debug] DOTNET: Registered Webcil assembly '/System.Private.CoreLib.dll'
[debug] DOTNET: Registered Webcil assembly '/System.Console.dll'
[debug] DOTNET: Registered Webcil assembly '/icu_Release_False_<hash>.dll'
[debug] DOTNET: added module imports 'main.js'
[debug] DOTNET: Assembly not found: 'System.Runtime.InteropServices.JavaScript.dll'
[error] DOTNET: Unhandled error: [object WebAssembly.Exception]

Root cause

System.Runtime.InteropServices.JavaScript.dll is missing from the publish output. Listing wwwroot/_framework:

System.Console.lo81b7e4b7.wasm
System.Private.CoreLib.34bdiwqb55.wasm
icu_Release_False_<hash>.esu2dv7gw6.wasm
dotnet.native.s0150lj7eq.wasm

No System.Runtime.InteropServices.JavaScript.wasm.

Using CDP Debugger.setPauseOnExceptions: 'all', the WebAssembly.Exception escapes out of _SystemInteropJS_BindAssemblyExports on the first JS→managed call. That UnmanagedCallersOnly export lives in JavaScriptExports.CoreCLR.cs:127, but its assembly isn't deployed, so the runtime fails before the C# try { ... } catch { argException.ToJS(ex); } is reached.

CDP-captured wasm stack at the throw (one of multiple internal throws ultimately escaping):

$func188 → $func694 → $func5266 → $func1218 → $func9578 → $func7334 → $func2994 → $func2225 →
$func156 → $func9207 → $func1714 → $func155 → $Ma →
_SystemInteropJS_BindAssemblyExports → SystemInteropJS_BindAssemblyExports → ...

Implications for this PR

These tests (IcuTests.FullIcuFromRuntimePackWithInvariant, FullIcuFromRuntimePackWithCustomIcu, InvariantGlobalizationTests.*) already use [TestCategory("native-coreclr")], so removing the class-level [TestCategory("native")] filter in this PR did not cause them to start running on CoreCLR — they were already in the CoreCLR set. The failure is a pre-existing publish/runtime-pack issue: System.Runtime.InteropServices.JavaScript isn't being included for CoreCLR-wasm test apps.

The fix likely belongs either in the CoreCLR browser-wasm runtime pack metadata (so System.Runtime.InteropServices.JavaScript is treated as a required platform assembly on browser-wasm) or in the WBT publish targets for non-workload CoreCLR scenarios — not in WBT test categorization.

Group A (DllImportTests / NativeBuildTests / NativeLibraryTests / PInvokeTableGeneratorTests) is a separate, unrelated issue caused by -include coreclr_compat.h being applied to user .c files; I have a fix locally for BrowserWasmApp.CoreCLR.targets and can push it on request.

Update: why System.Runtime.InteropServices.JavaScript.dll isn't in the publish output

I analyzed <test>-publish.binlog and confirmed: ILLink trims the assembly away during publish.

Evidence

  1. ILLink invocation (in the binlog: project 127, target 255, task 189):

    • -a "<TestApp>" EntryPoint — only the user app is rooted
    • --trim-mode link --action link — aggressive: any assembly not reachable from a root is removed entirely
    • System.Runtime.InteropServices.JavaScript.dll is passed only as -reference, not as -a/root
  2. ILLink linked output (obj/Release/net11.0/linked/) contains only three managed assemblies:

    System.Console.dll
    System.Private.CoreLib.dll
    <TestApp>.dll
    

    System.Runtime.InteropServices.JavaScript.dll is not present.

  3. The test-app source (a StopwatchSample-style program) doesn't reference any [JSImport]/[JSExport] types, so nothing in user code pulls the assembly in.

  4. The publish targets don't root it:

    • BrowserWasmApp.targets:54-60 only roots the user's own assembly (%(Identity)' == '$(AssemblyName)') and only when OutputType==Library.
    • BrowserWasmApp.CoreCLR.targets references libSystem.Runtime.InteropServices.JavaScript.Native.{a,js} (the native lib used by emcc), but never adds the managed System.Runtime.InteropServices.JavaScript as a TrimmerRootAssembly or <TrimmerRootDescriptor>.
  5. The native CoreCLR runtime (dotnet.native.wasm) unconditionally invokes the managed export SystemInteropJS_BindAssemblyExports during startup; that export lives in JavaScriptExports.CoreCLR.cs:125. Once the assembly has been trimmed, that call fails — matching the diagnostic-traced message:

    [debug] DOTNET: Assembly not found: 'System.Runtime.InteropServices.JavaScript.dll'
    

The static-web-asset pipeline still computes an endpoint for the assembly (_framework/System.Runtime.InteropServices.JavaScript.<hash>.wasm with IsBuildAsset/integrity), but the subsequent publish step filters down to the linked output, so the file never reaches wwwroot/_framework.

Why Mono doesn't hit this

Mono-on-Wasm uses a different publish pipeline (WasmAppBuilder + Blazor-Wasm SDK targets) that supplies its own descriptors/roots for JS interop assemblies, so System.Runtime.InteropServices.JavaScript survives trimming there. The CoreCLR-Wasm publish path in BrowserWasmApp.CoreCLR.targets is missing the equivalent.

Fix shape

Add a CoreCLR-WASM-specific TrimmerRoot in BrowserWasmApp.CoreCLR.targets, e.g.:

<Target Name="_RootJavaScriptInteropAssembly"
        AfterTargets="PrepareForILLink" BeforeTargets="_RunILLink"
        Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
  <ItemGroup>
    <TrimmerRootAssembly Include="System.Runtime.InteropServices.JavaScript">
      <RootMode>library</RootMode>
    </TrimmerRootAssembly>
  </ItemGroup>
</Target>

RootMode=library keeps the public surface; EntryPoint may be enough if only the UnmanagedCallersOnly exports are needed by the runtime, but library is the safer default given multiple managed entry points are called from dotnet.native.wasm startup.

Why the test fails

The ICU tests override Program.cs, but keep using main.js from template. It calls getAssemlyExports. The fix for the tests is to use a truncated main.js.

Anyway, we should improve the error message for cases when JS interop is trimmed.

maraf and others added 3 commits April 29, 2026 08:34
The ICU tests override Program.cs with code that does not use JS interop,
but kept the wasmbrowser template's main.js which calls setModuleImports
and getAssemblyExports. With CoreCLR-Wasm, the trimmer drops
System.Runtime.InteropServices.JavaScript because nothing managed roots
it, and JS startup then fails when trying to bind its exports.

Add a minimal icu_main.js tailored for ICU tests (no JS interop calls,
runMainAndExit, diagnostic tracing + console forwarding) and have
CreateIcuProject overwrite the template main.js with it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The force-include of coreclr_compat.h is required for the
ManagedToNativeGenerator output (pinvoke-table.cpp,
wasm_m2n_invoke.g.cpp), but it breaks user .c sources brought in via
NativeFileReference: the compat header uses C++-style un-tagged struct
references that don't compile in C.

Split the emcc invocation into two passes with separate RSP files:
- user NativeFileReference sources (.c/.cpp) without -include compat
- generated cpp from ManagedToNativeGenerator with -include compat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ble-non-aot-tests

# Conflicts:
#	eng/testing/scenarios/BuildWasmAppsJobsListCoreCLR.txt
Copilot AI review requested due to automatic review settings April 29, 2026 09:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs:104

  • EnsureWasmTemplatesInstalled may install templates into a fallback DOTNET_CLI_HOME under BuildEnvironment.TmpPath when the process doesn't already have DOTNET_CLI_HOME set, but the subsequent dotnet new <template> call uses DotNetCommand(useDefaultArgs:false) and does not set DOTNET_CLI_HOME. In that case dotnet new will look in the default CLI home and won't see the installed templates. Consider explicitly setting DOTNET_CLI_HOME on the dotnet new invocation (or setting it for the current process) to guarantee both steps share the same template cache in local runs.
        EnsureWasmTemplatesInstalled();

        // [diag] Log DOTNET_CLI_HOME inherited by the `dotnet new <template>` invocation.
        // The template engine reads installed templates from this location; if it differs
        // from the DOTNET_CLI_HOME used by EnsureWasmTemplatesInstalled, the template
        // won't be found.
        string inheritedCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME") ?? "<unset>";
        string envVarsCliHome = s_buildEnv.EnvVars.TryGetValue("DOTNET_CLI_HOME", out string? evCliHome) ? evCliHome : "<unset-in-EnvVars>";
        Console.WriteLine($"[diag] DOTNET_CLI_HOME inherited by `dotnet new {template.ToString().ToLower()}`: '{inheritedCliHome}' (buildEnv.EnvVars: '{envVarsCliHome}')");

        using DotNetCommand cmd = new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false);
        CommandResult result = cmd.WithWorkingDirectory(_projectDir)
            .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
            .ExecuteWithCapturedOutput($"new {template.ToString().ToLower()} {extraArgs}")
            .EnsureSuccessful();

Comment on lines +1 to +3
// Auto-included CoreCLR compat header for app native build.
//
// This header is pre-included via -include when compiling pinvoke-table.cpp
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New header file is missing the standard .NET Foundation MIT license header comment used throughout the repo. Please add the usual "Licensed to the .NET Foundation under one or more agreements..." block at the top for consistency/compliance.

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +136
// The template main.js calls JS interop APIs (setModuleImports, getAssemblyExports)
// which the ICU test program does not use. Replace it with a minimal version tailored
// for ICU tests, otherwise the JS interop assembly would be linked away by the trimmer.
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comment says the template main.js would cause the JS interop assembly to be linked away by the trimmer, but the reason to replace main.js seems to be the opposite: avoid calling JS interop APIs (setModuleImports/getAssemblyExports) so the app doesn't depend on JS interop being preserved. Please clarify the comment to reflect the actual failure mode/intent (e.g., default main.js calls these APIs but the app doesn't reference the managed JS interop pieces, so trimming can remove them and the default main.js would then fail at runtime).

Suggested change
// The template main.js calls JS interop APIs (setModuleImports, getAssemblyExports)
// which the ICU test program does not use. Replace it with a minimal version tailored
// for ICU tests, otherwise the JS interop assembly would be linked away by the trimmer.
// The template main.js calls JS interop APIs (setModuleImports, getAssemblyExports),
// but the ICU test program does not reference the managed JS interop pieces. Trimming
// can therefore remove them, and the default main.js would then fail at runtime, so
// replace it with a minimal version tailored for ICU tests that avoids those APIs.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to 18
[TestCategory("native-coreclr")]
public class MemoryTests : WasmTemplateTestsBase
{
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the class category changed to "native-coreclr", this test class will now run on the CoreCLR WBT lane (which sets SDK_HAS_WORKLOAD_INSTALLED=false). In that lane, AllocateLargeHeapThenRepeatedlyInterop calls BuildProject(... ExpectSuccess: BuildTestBase.IsUsingWorkloads) which becomes false, so the test effectively expects the build to fail and then doesn't run/validate anything. If the intent is to start exercising this scenario on CoreCLR, consider splitting the workload/no-workload variants (or tagging the workload-only [Fact] with TestCategory("workload")) and making the CoreCLR-enabled path assert a successful native rebuild instead of treating failure as success.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Build-mono os-browser Browser variant of arch-wasm test-enhancement Improvements of test source code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants