This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a commit to branch 
Feature/162-quiet-app-settings-in-a-native-host
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit 24457a61a1597541595a1b374af7c3daa61dd752
Author: Jan Friedrich <[email protected]>
AuthorDate: Wed Aug 19 22:42:48 2026 +0200

    Guard the native host case in CI (#162)
    
    The unit test for a natively hosted process builds the exception by hand, 
so it
    asserts what we believe the configuration system does rather than what it 
does.
    This adds a check that removes the entry assembly for real and reports what
    log4net writes while starting up, in the project that already exists to 
record
    what log4net can do in an unusual host.
    
    It cannot be one of the probes in the list: the configuration system caches 
its
    initialization and log4net reads its first application setting from a static
    constructor, so by the time any probe runs the outcome is already decided. 
The
    check therefore owns the process from its first statement and the runner 
selects
    it with an argument.
    
    Verified in both directions: it passes as committed, and reverting the
    PlatformNotSupportedException case makes it exit non-zero and print the 
seven
    error blocks it captured.
    
    JIT compiled only, deliberately. Native AOT trims the configuration system 
away,
    so the first setting read there fails as a missing constructor long before 
it can
    fail for want of an entry assembly: the published executable passes this 
check
    even with the fix reverted, which would be a green that cannot fail for the
    reason the check exists. The reason is recorded next to the CI step and on 
the
    class, so it does not get added back for symmetry.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .github/workflows/build.yaml             |  16 +++
 src/log4net.Tests.Aot/NativeHostCheck.cs | 189 +++++++++++++++++++++++++++++++
 src/log4net.Tests.Aot/Program.cs         |  10 +-
 3 files changed, 214 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 2599ffcd..af5b3359 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -72,4 +72,20 @@ jobs:
             dotnet publish ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c 
Release -o ./aot-probes
             if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
             & "./aot-probes/log4net.Tests.Aot$($IsWindows ? '.exe' : '')"
+            if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+      # A process that hosts the runtime natively has no entry assembly, so 
the configuration
+      # system cannot work out where the config file is (issue #162). The 
check owns the process
+      # from its first statement, because the configuration system caches its 
initialization and
+      # log4net reads its first setting from a static constructor, so it 
cannot be one of the
+      # probes above.
+      #
+      # JIT only, deliberately: under Native AOT the configuration system is 
trimmed away, so the
+      # first setting read fails as a missing constructor long before it can 
fail for want of an
+      # entry assembly. The published executable passes this check with the 
fix reverted, which
+      # makes it a green that cannot fail for the reason the check exists.
+      - name: AOT probes, no entry assembly
+        shell: pwsh
+        run: |
+            dotnet run --project 
./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -- 
--no-entry-assembly
             if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
\ No newline at end of file
diff --git a/src/log4net.Tests.Aot/NativeHostCheck.cs 
b/src/log4net.Tests.Aot/NativeHostCheck.cs
new file mode 100644
index 00000000..d799ffea
--- /dev/null
+++ b/src/log4net.Tests.Aot/NativeHostCheck.cs
@@ -0,0 +1,189 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+using log4net.Appender;
+using log4net.Config;
+using log4net.Core;
+using log4net.Layout;
+using log4net.Util;
+
+namespace log4net.Tests.Aot;
+
+/// <summary>
+/// Records what log4net does in a process that hosts the runtime natively, 
where there is no entry
+/// assembly for the configuration system to derive the config file path from 
(issue #162).
+/// </summary>
+/// <remarks>
+/// <para>
+/// This cannot be one of <see cref="Probes"/>: the configuration system 
caches its initialization,
+/// and log4net reads its first application setting from a static constructor, 
so by the time any
+/// probe runs the outcome is already decided. The check therefore owns the 
process from its first
+/// statement, which is why the runner invokes it instead of the probe list.
+/// </para>
+/// <para>
+/// Removing the entry assembly is what a native host does to the process, and 
reflecting onto
+/// <c>Assembly.SetEntryAssembly</c> is the only way to arrive there without 
one. It is a CoreCLR
+/// internal, so the check reports itself as not applicable rather than 
failing where it is absent.
+/// </para>
+/// <para>
+/// The build runs this JIT compiled only. Native AOT trims the configuration 
system away, so there
+/// the first setting read fails as a missing constructor before it can fail 
for want of an entry
+/// assembly: the published executable passes this check even with the fix for 
#162 reverted, which
+/// would be a green that cannot fail for the reason the check exists.
+/// </para>
+/// </remarks>
+internal static class NativeHostCheck
+{
+  /// <summary>
+  /// The argument that selects this check instead of the probe list.
+  /// </summary>
+  internal const string Argument = "--no-entry-assembly";
+
+  /// <summary>
+  /// A setting that only app.config carries, so reading it proves whether the 
configuration system
+  /// was really bypassed rather than answering from a cache.
+  /// </summary>
+  private const string ConfigOnlyKey = "log4net.AotProbe";
+
+  /// <summary>
+  /// Removes the entry assembly, starts log4net, and reports what it wrote 
while doing so.
+  /// </summary>
+  /// <returns>0 if log4net started without reporting an error, otherwise 
1</returns>
+  internal static int Run()
+  {
+    Console.WriteLine("log4net probes, no entry assembly");
+    Console.WriteLine(new string('-', 100));
+
+    if (RemoveTheEntryAssembly() is string unavailable)
+    {
+      Console.WriteLine($"  {"n/a",-6} settings/no error without an entry 
assembly");
+      Console.WriteLine($"         {unavailable}");
+      Console.WriteLine(new string('-', 100));
+      Console.WriteLine("no entry assembly: not applicable here");
+      return 0;
+    }
+
+    // log4net reports an unreadable configuration system on Console.Error, 
from a static
+    // constructor, so the writer has to be in place before anything touches 
log4net at all.
+    TextWriter console = Console.Error;
+    using StringWriter emitted = new();
+    Console.SetError(emitted);
+    List<string> failures = [];
+    try
+    {
+      Environment.SetEnvironmentVariable(Program.EnvironmentProbeKey, 
"from-environment");
+      Check(failures, SystemInfo.GetAppSetting(Program.EnvironmentProbeKey) == 
"from-environment",
+        "an application setting was not read from the environment");
+      Check(failures, SystemInfo.GetAppSetting(ConfigOnlyKey) is null,
+        $"{ConfigOnlyKey} was answered from app.config, so this check 
exercised nothing");
+      Check(failures, Log() == 1, "the event did not reach the appender");
+    }
+    catch (Exception e) when (e is not (OutOfMemoryException or 
StackOverflowException))
+    {
+      failures.Add($"{e.GetType().Name}: {e.Message}");
+    }
+    finally
+    {
+      Console.SetError(console);
+    }
+
+    // The symptom of #162 is a stack trace for every setting log4net reads, 
in an application whose
+    // configuration is fine. An absent configuration system is a property of 
the host, so it
+    // belongs at debug level, and nothing here turns internal debugging on.
+    string output = emitted.ToString();
+    Check(failures, !output.Contains("log4net:ERROR", 
StringComparison.Ordinal),
+      "log4net reported an error while reading its application settings");
+
+    foreach (string failure in failures)
+    {
+      Console.WriteLine($"  {"FAIL",-6} settings/no error without an entry 
assembly");
+      Console.WriteLine($"         {failure}");
+    }
+    if (failures.Count == 0)
+    {
+      Console.WriteLine($"  {"ok",-6} settings/no error without an entry 
assembly");
+    }
+    else if (output.Length > 0)
+    {
+      Console.WriteLine("what log4net wrote:");
+      Console.WriteLine(output);
+    }
+
+    Console.WriteLine(new string('-', 100));
+    Console.WriteLine(failures.Count == 0
+      ? "no entry assembly: log4net started without reporting an error"
+      : $"no entry assembly: {failures.Count} check(s) did not match 
expectations");
+    return failures.Count == 0 ? 0 : 1;
+  }
+
+  /// <summary>
+  /// Makes this process look like a natively hosted one.
+  /// </summary>
+  /// <returns>null once there is no entry assembly, otherwise why that cannot 
be arranged</returns>
+  private static string? RemoveTheEntryAssembly()
+  {
+    MethodInfo? setEntryAssembly = typeof(Assembly)
+      .GetMethod("SetEntryAssembly", BindingFlags.Static | BindingFlags.Public 
| BindingFlags.NonPublic);
+    if (setEntryAssembly is null)
+    {
+      return "Assembly.SetEntryAssembly is not available, so the entry 
assembly cannot be removed";
+    }
+    try
+    {
+      setEntryAssembly.Invoke(null, [null]);
+    }
+    catch (Exception e) when (e is not (OutOfMemoryException or 
StackOverflowException))
+    {
+      return $"Assembly.SetEntryAssembly rejected the call: 
{e.InnerException?.Message ?? e.Message}";
+    }
+    return Assembly.GetEntryAssembly() is null
+      ? null
+      : "Assembly.SetEntryAssembly left an entry assembly in place";
+  }
+
+  /// <summary>
+  /// Starts log4net the way an application would, which is what reads the 
application settings.
+  /// </summary>
+  /// <returns>the number of events that reached the appender</returns>
+  private static int Log()
+  {
+    MemoryAppender memory = new()
+    {
+      Layout = new PatternLayout("%level %logger %message"),
+      Threshold = Level.All,
+    };
+    memory.ActivateOptions();
+    BasicConfigurator.Configure(memory);
+    LogManager.GetLogger(typeof(NativeHostCheck)).Info("hello from a host 
without an entry assembly");
+    return memory.GetEvents().Length;
+  }
+
+  private static void Check(List<string> failures, bool condition, string 
message)
+  {
+    if (!condition)
+    {
+      failures.Add(message);
+    }
+  }
+}
diff --git a/src/log4net.Tests.Aot/Program.cs b/src/log4net.Tests.Aot/Program.cs
index 5f484cb4..bbe262cb 100644
--- a/src/log4net.Tests.Aot/Program.cs
+++ b/src/log4net.Tests.Aot/Program.cs
@@ -19,6 +19,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
 
 using log4net.Util;
@@ -42,8 +43,15 @@ internal static class Program
   /// </summary>
   internal const string EnvironmentProbeKey = "log4net.AotEnvironmentProbe";
 
-  private static int Main()
+  private static int Main(string[] args)
   {
+    if (args.Contains(NativeHostCheck.Argument))
+    {
+      // Owns the process from here: it has to remove the entry assembly 
before anything reads a
+      // setting, so it cannot share a run with the probes.
+      return NativeHostCheck.Run();
+    }
+
     // The probes report their own failures, so log4net's internal error 
reporting is only noise
     // here, and a passing run that prints errors reads as a broken one.
     LogLog.EmitInternalMessages = false;

Reply via email to