1 // Copyright (c) Microsoft. All rights reserved. 2 // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 4 #pragma warning disable 436 5 6 using System; 7 using System.IO; 8 using System.Collections.Generic; 9 using System.Text; 10 11 using Microsoft.Build.Engine.UnitTests; 12 using Microsoft.Build.Framework; 13 using Microsoft.Build.Logging; 14 using Microsoft.Build.Shared; 15 16 17 18 using EventSourceSink = Microsoft.Build.BackEnd.Logging.EventSourceSink; 19 using Project = Microsoft.Build.Evaluation.Project; 20 using Xunit; 21 22 namespace Microsoft.Build.UnitTests 23 { 24 public class FileLogger_Tests 25 { 26 /// <summary> 27 /// Basic test of the file logger. Writes to a log file in the temp directory. 28 /// </summary> 29 [Fact] Basic()30 public void Basic() 31 { 32 FileLogger fileLogger = new FileLogger(); 33 string logFile = FileUtilities.GetTemporaryFile(); 34 fileLogger.Parameters = "verbosity=Normal;logfile=" + logFile; 35 36 Project project = ObjectModelHelpers.CreateInMemoryProject(@" 37 <Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`> 38 <Target Name=`Build`> 39 <Message Text=`Hello world from the FileLogger`/> 40 </Target> 41 </Project> 42 "); 43 44 project.Build(fileLogger); 45 46 project.ProjectCollection.UnregisterAllLoggers(); 47 48 string log = File.ReadAllText(logFile); 49 Assert.True(log.Contains("Hello world from the FileLogger")); // "Log should have contained message" 50 51 File.Delete(logFile); 52 } 53 54 /// <summary> 55 /// Basic case of logging a message to a file 56 /// Verify it logs and encoding is ANSI 57 /// </summary> 58 [Fact] BasicNoExistingFile()59 public void BasicNoExistingFile() 60 { 61 string log = null; 62 63 try 64 { 65 log = GetTempFilename(); 66 SetUpFileLoggerAndLogMessage("logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 67 VerifyFileContent(log, "message here"); 68 69 70 byte[] content = ReadRawBytes(log); 71 Assert.Equal((byte)109, content[0]); // 'm' 72 } 73 finally 74 { 75 if (null != log) File.Delete(log); 76 } 77 } 78 79 /// <summary> 80 /// Invalid file should error nicely 81 /// </summary> 82 [Fact] 83 [Trait("Category", "netcore-osx-failing")] 84 [Trait("Category", "netcore-linux-failing")] 85 [Trait("Category", "mono-osx-failing")] InvalidFile()86 public void InvalidFile() 87 { 88 Assert.Throws<LoggerException>(() => 89 { 90 string log = null; 91 92 try 93 { 94 SetUpFileLoggerAndLogMessage("logfile=||invalid||", new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 95 } 96 finally 97 { 98 if (null != log) File.Delete(log); 99 } 100 } 101 ); 102 } 103 /// <summary> 104 /// Specific verbosity overrides global verbosity 105 /// </summary> 106 [Fact] SpecificVerbosity()107 public void SpecificVerbosity() 108 { 109 string log = null; 110 111 try 112 { 113 log = GetTempFilename(); 114 FileLogger fl = new FileLogger(); 115 EventSourceSink es = new EventSourceSink(); 116 fl.Parameters = "verbosity=diagnostic;logfile=" + log; // diagnostic specific setting 117 fl.Verbosity = LoggerVerbosity.Quiet; // quiet global setting 118 fl.Initialize(es); 119 fl.MessageHandler(null, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 120 fl.Shutdown(); 121 122 // expect message to appear because diagnostic not quiet verbosity was used 123 VerifyFileContent(log, "message here"); 124 } 125 finally 126 { 127 if (null != log) File.Delete(log); 128 } 129 } 130 131 /// <summary> 132 /// Test the short hand verbosity settings for the file logger 133 /// </summary> 134 [Fact] ValidVerbosities()135 public void ValidVerbosities() 136 { 137 string[] verbositySettings = new string[] { "Q", "quiet", "m", "minimal", "N", "normal", "d", "detailed", "diag", "DIAGNOSTIC" }; 138 LoggerVerbosity[] verbosityEnumerations = new LoggerVerbosity[] {LoggerVerbosity.Quiet, LoggerVerbosity.Quiet, 139 LoggerVerbosity.Minimal, LoggerVerbosity.Minimal, 140 LoggerVerbosity.Normal, LoggerVerbosity.Normal, 141 LoggerVerbosity.Detailed, LoggerVerbosity.Detailed, 142 LoggerVerbosity.Diagnostic, LoggerVerbosity.Diagnostic}; 143 for (int i = 0; i < verbositySettings.Length; i++) 144 { 145 FileLogger fl = new FileLogger(); 146 fl.Parameters = "verbosity=" + verbositySettings[i] + ";"; 147 EventSourceSink es = new EventSourceSink(); 148 fl.Initialize(es); 149 fl.Shutdown(); 150 Assert.Equal(fl.Verbosity, verbosityEnumerations[i]); 151 } 152 153 // Do the same using the v shorthand 154 for (int i = 0; i < verbositySettings.Length; i++) 155 { 156 FileLogger fl = new FileLogger(); 157 fl.Parameters = "v=" + verbositySettings[i] + ";"; 158 EventSourceSink es = new EventSourceSink(); 159 fl.Initialize(es); 160 fl.Shutdown(); 161 Assert.Equal(fl.Verbosity, verbosityEnumerations[i]); 162 } 163 } 164 165 /// <summary> 166 /// Invalid verbosity setting 167 /// </summary> 168 [Fact] InvalidVerbosity()169 public void InvalidVerbosity() 170 { 171 Assert.Throws<LoggerException>(() => 172 { 173 FileLogger fl = new FileLogger(); 174 fl.Parameters = "verbosity=CookiesAndCream"; 175 EventSourceSink es = new EventSourceSink(); 176 fl.Initialize(es); 177 } 178 ); 179 } 180 /// <summary> 181 /// Invalid encoding setting 182 /// </summary> 183 [Fact] InvalidEncoding()184 public void InvalidEncoding() 185 { 186 Assert.Throws<LoggerException>(() => 187 { 188 string log = null; 189 190 try 191 { 192 log = GetTempFilename(); 193 FileLogger fl = new FileLogger(); 194 EventSourceSink es = new EventSourceSink(); 195 fl.Parameters = "encoding=foo;logfile=" + log; 196 fl.Initialize(es); 197 } 198 finally 199 { 200 if (null != log) File.Delete(log); 201 } 202 } 203 ); 204 } 205 206 /// <summary> 207 /// Valid encoding setting 208 /// </summary> 209 [Fact] ValidEncoding()210 public void ValidEncoding() 211 { 212 string log = null; 213 214 try 215 { 216 log = GetTempFilename(); 217 SetUpFileLoggerAndLogMessage("encoding=utf-16;logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 218 byte[] content = ReadRawBytes(log); 219 220 // FF FE is the BOM for UTF16 221 Assert.Equal((byte)255, content[0]); 222 Assert.Equal((byte)254, content[1]); 223 } 224 finally 225 { 226 if (null != log) File.Delete(log); 227 } 228 } 229 230 /// <summary> 231 /// Valid encoding setting 232 /// </summary> 233 [Fact] ValidEncoding2()234 public void ValidEncoding2() 235 { 236 string log = null; 237 238 try 239 { 240 log = GetTempFilename(); 241 SetUpFileLoggerAndLogMessage("encoding=utf-8;logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 242 byte[] content = ReadRawBytes(log); 243 244 // EF BB BF is the BOM for UTF8 245 Assert.Equal((byte)239, content[0]); 246 Assert.Equal((byte)187, content[1]); 247 Assert.Equal((byte)191, content[2]); 248 } 249 finally 250 { 251 if (null != log) File.Delete(log); 252 } 253 } 254 255 /// <summary> 256 /// Read the raw byte content of a file 257 /// </summary> 258 /// <param name="log"></param> 259 /// <returns></returns> ReadRawBytes(string log)260 private byte[] ReadRawBytes(string log) 261 { 262 byte[] content; 263 using (FileStream stream = new FileStream(log, FileMode.Open)) 264 { 265 content = new byte[stream.Length]; 266 267 for (int i = 0; i < stream.Length; i++) 268 { 269 content[i] = (byte)stream.ReadByte(); 270 } 271 } 272 273 return content; 274 } 275 276 /// <summary> 277 /// Logging a message to a file that already exists should overwrite it 278 /// </summary> 279 [Fact] BasicExistingFileNoAppend()280 public void BasicExistingFileNoAppend() 281 { 282 string log = null; 283 284 try 285 { 286 log = GetTempFilename(); 287 WriteContentToFile(log); 288 SetUpFileLoggerAndLogMessage("logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 289 VerifyFileContent(log, "message here"); 290 } 291 finally 292 { 293 if (null != log) File.Delete(log); 294 } 295 } 296 297 /// <summary> 298 /// Logging to a file that already exists, with "append" set, should append 299 /// </summary> 300 [Fact] BasicExistingFileAppend()301 public void BasicExistingFileAppend() 302 { 303 string log = null; 304 305 try 306 { 307 log = GetTempFilename(); 308 WriteContentToFile(log); 309 SetUpFileLoggerAndLogMessage("append;logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 310 VerifyFileContent(log, "existing content\nmessage here"); 311 } 312 finally 313 { 314 if (null != log) File.Delete(log); 315 } 316 } 317 318 /// <summary> 319 /// Logging to a file in a directory that doesn't exists 320 /// </summary> 321 [Fact] BasicNoExistingDirectory()322 public void BasicNoExistingDirectory() 323 { 324 string directory = Path.Combine(ObjectModelHelpers.TempProjectDir, Guid.NewGuid().ToString("N")); 325 string log = Path.Combine(directory, "build.log"); 326 Assert.False(Directory.Exists(directory)); 327 Assert.False(File.Exists(log)); 328 329 try 330 { 331 SetUpFileLoggerAndLogMessage("logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); 332 VerifyFileContent(log, "message here"); 333 } 334 finally 335 { 336 ObjectModelHelpers.DeleteDirectory(directory); 337 } 338 } 339 340 [Theory] 341 [InlineData("warningsonly")] 342 [InlineData("errorsonly")] 343 [InlineData("errorsonly;warningsonly")] EmptyErrorLogUsingWarningsErrorsOnly(string loggerOption)344 public void EmptyErrorLogUsingWarningsErrorsOnly(string loggerOption) 345 { 346 using (var env = TestEnvironment.Create()) 347 { 348 var logFile = env.CreateFile(".log").Path; 349 350 // Note: Only the ParallelConsoleLogger supports this scenario (log file empty on no error/warn). We 351 // need to explicitly enable it here with the 'ENABLEMPLOGGING' flag. 352 FileLogger fileLogger = new FileLogger {Parameters = $"{loggerOption};logfile={logFile};ENABLEMPLOGGING" }; 353 354 Project project = ObjectModelHelpers.CreateInMemoryProject(@" 355 <Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`> 356 <Target Name=`Build`> 357 <Message Text=`Hello world from the FileLogger`/> 358 </Target> 359 </Project>"); 360 361 project.Build(fileLogger); 362 project.ProjectCollection.UnregisterAllLoggers(); 363 364 // File should exist and be 0 length (no summary information, etc.) 365 var result = new FileInfo(logFile); 366 Assert.True(result.Exists); 367 Assert.Equal(0, new FileInfo(logFile).Length); 368 } 369 } 370 371 /// <summary> 372 /// Gets a filename for a nonexistent temporary file. 373 /// </summary> 374 /// <returns></returns> GetTempFilename()375 private string GetTempFilename() 376 { 377 string path = FileUtilities.GetTemporaryFile(); 378 File.Delete(path); 379 return path; 380 } 381 382 /// <summary> 383 /// Writes a string to a file. 384 /// </summary> 385 /// <param name="log"></param> WriteContentToFile(string log)386 private void WriteContentToFile(string log) 387 { 388 using (StreamWriter sw = FileUtilities.OpenWrite(log, false)) 389 { 390 sw.WriteLine("existing content"); 391 } 392 } 393 394 /// <summary> 395 /// Creates a FileLogger, sets its parameters and initializes it, 396 /// logs a message to it, and calls shutdown 397 /// </summary> 398 /// <param name="parameters"></param> 399 /// <returns></returns> SetUpFileLoggerAndLogMessage(string parameters, BuildMessageEventArgs message)400 private void SetUpFileLoggerAndLogMessage(string parameters, BuildMessageEventArgs message) 401 { 402 FileLogger fl = new FileLogger(); 403 EventSourceSink es = new EventSourceSink(); 404 fl.Parameters = parameters; 405 fl.Initialize(es); 406 fl.MessageHandler(null, message); 407 fl.Shutdown(); 408 return; 409 } 410 411 /// <summary> 412 /// Verifies that a file contains exactly the expected content. 413 /// </summary> 414 /// <param name="file"></param> 415 /// <param name="expectedContent"></param> VerifyFileContent(string file, string expectedContent)416 private void VerifyFileContent(string file, string expectedContent) 417 { 418 string actualContent; 419 using (StreamReader sr = FileUtilities.OpenRead(file)) 420 { 421 actualContent = sr.ReadToEnd(); 422 } 423 424 string[] actualLines = actualContent.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); 425 string[] expectedLines = expectedContent.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); 426 427 Assert.Equal(expectedLines.Length, actualLines.Length); 428 429 for (int i = 0; i < expectedLines.Length; i++) 430 { 431 Assert.Equal(expectedLines[i].Trim(), actualLines[i].Trim()); 432 } 433 } 434 435 #region DistributedLogger 436 /// <summary> 437 /// Check the ability of the distributed logger to correctly tell its internal file logger where to log the file 438 /// </summary> 439 [Fact] DistributedFileLoggerParameters()440 public void DistributedFileLoggerParameters() 441 { 442 DistributedFileLogger fileLogger = new DistributedFileLogger(); 443 try 444 { 445 fileLogger.NodeId = 0; 446 fileLogger.Initialize(new EventSourceSink()); 447 Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=msbuild0.log;", StringComparison.OrdinalIgnoreCase)); 448 fileLogger.Shutdown(); 449 450 fileLogger.NodeId = 3; 451 fileLogger.Parameters = "logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile.log"); 452 fileLogger.Initialize(new EventSourceSink()); 453 Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile3.log") + ";", StringComparison.OrdinalIgnoreCase)); 454 fileLogger.Shutdown(); 455 456 fileLogger.NodeId = 4; 457 fileLogger.Parameters = "logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile.log"); 458 fileLogger.Initialize(new EventSourceSink()); 459 Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile4.log") + ";", StringComparison.OrdinalIgnoreCase)); 460 fileLogger.Shutdown(); 461 462 Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "tempura")); 463 fileLogger.NodeId = 1; 464 fileLogger.Parameters = "logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "tempura", "mylogfile.log"); 465 fileLogger.Initialize(new EventSourceSink()); 466 Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "tempura", "mylogfile1.log") + ";", StringComparison.OrdinalIgnoreCase)); 467 fileLogger.Shutdown(); 468 } 469 finally 470 { 471 if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "tempura"))) 472 { 473 File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "tempura", "mylogfile1.log")); 474 FileUtilities.DeleteWithoutTrailingBackslash(Path.Combine(Directory.GetCurrentDirectory(), "tempura")); 475 } 476 File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "mylogfile0.log")); 477 File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "mylogfile3.log")); 478 File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "mylogfile4.log")); 479 } 480 } 481 482 [Fact] DistributedLoggerNullEmpty()483 public void DistributedLoggerNullEmpty() 484 { 485 Assert.Throws<LoggerException>(() => 486 { 487 DistributedFileLogger fileLogger = new DistributedFileLogger(); 488 fileLogger.NodeId = 0; 489 fileLogger.Initialize(new EventSourceSink()); 490 491 fileLogger.NodeId = 1; 492 fileLogger.Parameters = "logfile="; 493 fileLogger.Initialize(new EventSourceSink()); 494 Assert.True(false); 495 } 496 ); 497 } 498 #endregion 499 500 } 501 } 502