|
| 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