Skip to content

Commit 1920bb9

Browse files
committed
test what works under Native AOT (#306)
Add log4net.Tests.Aot, which runs the same probes JIT compiled and published with PublishAot and fails when either run differs from the expected list, and run it from CI. Guard the trimmer annotations with unit tests, and fix the two IL3000 warnings a consumer sees by handling the empty Assembly.Location a single file application returns.
1 parent 364e0ee commit 1920bb9

11 files changed

Lines changed: 762 additions & 3 deletions

File tree

.github/workflows/build.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,20 @@ jobs:
5656
5757
- name: Test
5858
run: |
59-
dotnet test ./src/log4net.sln
59+
dotnet test ./src/log4net.sln
60+
61+
# Runs the same probes twice, JIT compiled and published with Native AOT, so that a
62+
# difference between the two is caused by AOT rather than by the platform. The project
63+
# knows which probes are expected to fail in each mode and exits non-zero when reality
64+
# does not match, in either direction.
65+
- name: AOT probes
66+
shell: pwsh
67+
env:
68+
"log4net.AotEnvironmentProbe": from-environment
69+
run: |
70+
dotnet run --project ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release
71+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
72+
dotnet publish ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -o ./aot-probes
73+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
74+
& "./aot-probes/log4net.Tests.Aot$($IsWindows ? '.exe' : '')"
75+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,6 @@ validate
254254
/graphify-out/.graphify_python
255255
/graphify-out/*.sig
256256
/graphify-out/20*
257+
258+
# output of the AOT probe step
259+
/aot-probes/

src/log4net.Tests.Aot/App.config

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one or more
4+
contributor license agreements. See the NOTICE file distributed with
5+
this work for additional information regarding copyright ownership.
6+
The ASF licenses this file to you under the Apache License, Version 2.0
7+
(the "License"); you may not use this file except in compliance with
8+
the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
<configuration>
19+
<appSettings>
20+
<add key="log4net.AotProbe" value="from-config" />
21+
</appSettings>
22+
</configuration>

src/log4net.Tests.Aot/Probe.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#region Apache License
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one or more
4+
// contributor license agreements. See the NOTICE file distributed with
5+
// this work for additional information regarding copyright ownership.
6+
// The ASF licenses this file to you under the Apache License, Version 2.0
7+
// (the "License"); you may not use this file except in compliance with
8+
// the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
#endregion
19+
20+
using System;
21+
22+
namespace log4net.Tests.Aot;
23+
24+
/// <summary>
25+
/// One named piece of log4net surface to exercise.
26+
/// </summary>
27+
/// <param name="Area">the grouping the probe belongs to</param>
28+
/// <param name="Name">what the probe exercises</param>
29+
/// <param name="Run">the probe itself, which throws to signal failure</param>
30+
internal sealed record Probe(string Area, string Name, Action Run)
31+
{
32+
/// <summary>
33+
/// Identifies the probe in the expected failure lists.
34+
/// </summary>
35+
internal string Key => $"{Area}/{Name}";
36+
}

src/log4net.Tests.Aot/Probes.cs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#region Apache License
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one or more
4+
// contributor license agreements. See the NOTICE file distributed with
5+
// this work for additional information regarding copyright ownership.
6+
// The ASF licenses this file to you under the Apache License, Version 2.0
7+
// (the "License"); you may not use this file except in compliance with
8+
// the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
#endregion
19+
20+
using System;
21+
using System.Collections.Generic;
22+
using System.IO;
23+
using System.Net;
24+
using System.Xml;
25+
26+
using log4net.Appender;
27+
using log4net.Config;
28+
using log4net.Core;
29+
using log4net.Filter;
30+
using log4net.Layout;
31+
using log4net.Repository;
32+
using log4net.Repository.Hierarchy;
33+
using log4net.Util;
34+
35+
namespace log4net.Tests.Aot;
36+
37+
/// <summary>
38+
/// The log4net surface this project exercises.
39+
/// </summary>
40+
/// <remarks>
41+
/// <para>
42+
/// Each probe is run identically whether the assembly was JIT compiled or published with Native
43+
/// AOT, so a difference between the two runs is an AOT effect rather than a platform one.
44+
/// </para>
45+
/// </remarks>
46+
internal static class Probes
47+
{
48+
/// <summary>
49+
/// Probes that are expected to fail under Native AOT, with the reason.
50+
/// </summary>
51+
/// <remarks>
52+
/// <para>
53+
/// Keep it honest: an unlisted failure and a listed probe that starts passing both fail the run.
54+
/// </para>
55+
/// </remarks>
56+
internal static readonly Dictionary<string, string> ExpectedAotFailures = new(StringComparer.Ordinal)
57+
{
58+
["config/XmlConfigurator.Configure(element)"]
59+
= "appender and layout types are named as strings in XML, so the trimmer removes them",
60+
["settings/appSettings from app.config"]
61+
= "System.Configuration cannot initialize once trimmed; environment variables stand in",
62+
};
63+
64+
/// <summary>
65+
/// Probes that are expected to fail when the assembly is JIT compiled.
66+
/// </summary>
67+
internal static readonly Dictionary<string, string> ExpectedJitFailures = new(StringComparer.Ordinal)
68+
{
69+
["settings/appSettings from environment"]
70+
= "the environment only stands in once the configuration system is known to be unavailable",
71+
};
72+
73+
/// <summary>
74+
/// Every probe, in the order they are run.
75+
/// </summary>
76+
/// <returns>the probes</returns>
77+
internal static IEnumerable<Probe> All()
78+
{
79+
MemoryAppender memory = Configure();
80+
return
81+
[
82+
.. Core(memory),
83+
.. Configuration(),
84+
.. Each("appender", Appenders(), Activate),
85+
.. Each("layout", Layouts(), layout => Render(layout)),
86+
.. Patterns(),
87+
.. Each("filter", Filters(), Activate),
88+
.. Settings(),
89+
.. Shutdown(),
90+
];
91+
}
92+
93+
/// <summary>
94+
/// Wires up the appender the core probes assert against.
95+
/// </summary>
96+
private static MemoryAppender Configure()
97+
{
98+
MemoryAppender memory = new()
99+
{
100+
Layout = new PatternLayout("%level %logger %message"),
101+
Threshold = Level.All,
102+
};
103+
memory.ActivateOptions();
104+
BasicConfigurator.Configure(memory);
105+
return memory;
106+
}
107+
108+
private static IEnumerable<Probe> Core(MemoryAppender memory)
109+
{
110+
yield return new("core", "GetLogger(Type)", () => LogManager.GetLogger(typeof(Probes)));
111+
yield return new("core", "GetLogger(string)", () => LogManager.GetLogger("by.name"));
112+
yield return new("core", "GetRepository()", () => LogManager.GetRepository());
113+
yield return new("core", "Exists", () => LogManager.Exists("by.name"));
114+
yield return new("core", "GetCurrentLoggers", () => LogManager.GetCurrentLoggers());
115+
yield return new("core", "CreateRepository(name)", () => LogManager.CreateRepository("probe-repository"));
116+
yield return new("core", "event reaches appender", () =>
117+
{
118+
memory.Clear();
119+
LogManager.GetLogger(typeof(Probes)).Info("hello");
120+
Require(memory.GetEvents().Length == 1, "the event did not reach the appender");
121+
});
122+
yield return new("core", "level lookup by name", () =>
123+
{
124+
Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
125+
Require(hierarchy.LevelMap["WARN"] is not null, "WARN is not in the level map");
126+
});
127+
yield return new("core", "context properties", () =>
128+
{
129+
ThreadContext.Properties["thread"] = 1;
130+
GlobalContext.Properties["global"] = 2;
131+
LogicalThreadContext.Properties["logical"] = 3;
132+
});
133+
}
134+
135+
private static IEnumerable<Probe> Configuration()
136+
{
137+
yield return new("config", "BasicConfigurator.Configure(appender)", () => BasicConfigurator.Configure(new MemoryAppender()));
138+
yield return new("config", "XmlConfigurator.Configure(element)", () =>
139+
{
140+
XmlDocument document = new();
141+
document.LoadXml("""
142+
<log4net>
143+
<appender name="probe" type="log4net.Appender.MemoryAppender" />
144+
<root><level value="ALL" /><appender-ref ref="probe" /></root>
145+
</log4net>
146+
""");
147+
ILoggerRepository repository = LogManager.CreateRepository("xml-repository");
148+
XmlConfigurator.Configure(repository, document.DocumentElement!);
149+
Require(repository.GetAppenders().Length > 0, "no appenders were created from the XML");
150+
});
151+
}
152+
153+
private static IEnumerable<Probe> Patterns()
154+
{
155+
yield return new("layout", "PatternLayout with every built-in converter", () =>
156+
{
157+
PatternLayout layout = new("%a %c %C %d %F %l %L %m %M %n %p %P %r %t %u %w %x %X %utcdate %exception %stacktrace %stacktracedetail");
158+
Require(Render(layout).Length > 0, "the layout rendered nothing");
159+
});
160+
yield return new("layout", "PatternString", () =>
161+
Require(new PatternString("%processid %appdomain %date{yyyy}").Format().Length > 0, "the pattern rendered nothing"));
162+
}
163+
164+
private static IEnumerable<Probe> Settings()
165+
{
166+
yield return new("settings", "appSettings from app.config", () =>
167+
Require(SystemInfo.GetAppSetting("log4net.AotProbe") == "from-config", "the key was not read from app.config"));
168+
yield return new("settings", "appSettings from environment", () =>
169+
Require(SystemInfo.GetAppSetting(Program.EnvironmentProbeKey) == "from-environment", "the key was not read from the environment"));
170+
}
171+
172+
private static IEnumerable<Probe> Shutdown()
173+
{
174+
yield return new("shutdown", "Flush", () => LogManager.Flush(1000));
175+
yield return new("shutdown", "Shutdown", LogManager.Shutdown);
176+
}
177+
178+
/// <summary>
179+
/// One probe per entry, each creating its subject and handing it to <paramref name="check"/>.
180+
/// </summary>
181+
private static IEnumerable<Probe> Each<T>(string area, Dictionary<string, Func<T>> subjects, Action<T> check)
182+
{
183+
foreach (KeyValuePair<string, Func<T>> subject in subjects)
184+
{
185+
yield return new(area, subject.Key, () => check(subject.Value()));
186+
}
187+
}
188+
189+
private static Dictionary<string, Func<IAppender>> Appenders() => new(StringComparer.Ordinal)
190+
{
191+
["ConsoleAppender"] = () => new ConsoleAppender { Layout = new SimpleLayout() },
192+
["MemoryAppender"] = () => new MemoryAppender(),
193+
["DebugAppender"] = () => new DebugAppender { Layout = new SimpleLayout() },
194+
["TraceAppender"] = () => new TraceAppender { Layout = new SimpleLayout() },
195+
["ForwardingAppender"] = () => new ForwardingAppender(),
196+
["BufferingForwardingAppender"] = () => new BufferingForwardingAppender { BufferSize = 2 },
197+
["FileAppender"] = () => new FileAppender { File = TempFile("aot-probe-file.log"), Layout = new SimpleLayout() },
198+
["RollingFileAppender"] = () => new RollingFileAppender { File = TempFile("aot-probe-roll.log"), Layout = new SimpleLayout() },
199+
["UdpAppender"] = () => new UdpAppender { RemoteAddress = IPAddress.Loopback, RemotePort = 9999, Layout = new SimpleLayout() },
200+
["AnsiColorTerminalAppender"] = () => new AnsiColorTerminalAppender { Layout = new SimpleLayout() },
201+
};
202+
203+
private static Dictionary<string, Func<ILayout>> Layouts() => new(StringComparer.Ordinal)
204+
{
205+
["SimpleLayout"] = () => new SimpleLayout(),
206+
["PatternLayout"] = () => new PatternLayout("%level %logger %message%newline"),
207+
["ExceptionLayout"] = () => new ExceptionLayout(),
208+
["XmlLayout"] = () => new XmlLayout(),
209+
};
210+
211+
private static Dictionary<string, Func<IFilter>> Filters() => new(StringComparer.Ordinal)
212+
{
213+
["LevelRangeFilter"] = () => new LevelRangeFilter { LevelMin = Level.Debug, LevelMax = Level.Fatal },
214+
["LevelMatchFilter"] = () => new LevelMatchFilter { LevelToMatch = Level.Info },
215+
["StringMatchFilter"] = () => new StringMatchFilter { StringToMatch = "probe" },
216+
["PropertyFilter"] = () => new PropertyFilter { Key = "probe", StringToMatch = "value" },
217+
["DenyAllFilter"] = () => new DenyAllFilter(),
218+
};
219+
220+
private static string TempFile(string name) => Path.Combine(Path.GetTempPath(), name);
221+
222+
private static void Activate(object option) => (option as IOptionHandler)?.ActivateOptions();
223+
224+
private static string Render(ILayout layout)
225+
{
226+
Activate(layout);
227+
using StringWriter writer = new();
228+
layout.Format(writer, new(new()
229+
{
230+
Level = Level.Info,
231+
LoggerName = "probe",
232+
Message = "message",
233+
TimeStampUtc = DateTime.UtcNow,
234+
}));
235+
return writer.ToString();
236+
}
237+
238+
private static void Require(bool condition, string message)
239+
{
240+
if (!condition)
241+
{
242+
throw new InvalidOperationException(message);
243+
}
244+
}
245+
}

0 commit comments

Comments
 (0)