diff --git a/PerfView.sln b/PerfView.sln index e55ed9e79..acffcf78b 100644 --- a/PerfView.sln +++ b/PerfView.sln @@ -87,6 +87,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1CAEF854-292 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerfView.Tutorial", "src\PerfView.Tutorial\PerfView.Tutorial.csproj", "{DE35BED9-0E03-4DAC-A003-1ACBBF816973}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceParserGen.Tests", "src\TraceParserGen.Tests\TraceParserGen.Tests.csproj", "{F127C664-2F56-429B-BAA6-636034F766EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -405,12 +407,25 @@ Global {DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Release|x64.Build.0 = Release|Any CPU {DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Release|x86.ActiveCfg = Release|Any CPU {DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Release|x86.Build.0 = Release|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x64.Build.0 = Debug|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x86.Build.0 = Debug|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Release|Any CPU.Build.0 = Release|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x64.ActiveCfg = Release|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x64.Build.0 = Release|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x86.ActiveCfg = Release|Any CPU + {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {DE35BED9-0E03-4DAC-A003-1ACBBF816973} = {1CAEF854-2923-45FA-ACB8-6523A7E45896} + {F127C664-2F56-429B-BAA6-636034F766EF} = {1CAEF854-2923-45FA-ACB8-6523A7E45896} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F85A2A3-E0DF-4826-9BBA-4DFFA0F17150} diff --git a/src/TraceParserGen.Tests/ParserGenerationTests.cs b/src/TraceParserGen.Tests/ParserGenerationTests.cs new file mode 100644 index 000000000..a8293a003 --- /dev/null +++ b/src/TraceParserGen.Tests/ParserGenerationTests.cs @@ -0,0 +1,294 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +namespace TraceParserGen.Tests +{ + /// + /// Tests for TraceParserGen.exe that validate it can generate parsers from manifests + /// + public class ParserGenerationTests : TestBase + { + public ParserGenerationTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CanGenerateParserFromManifest() + { + // Arrange + string manifestPath = Path.Combine(TestDataDir, "SimpleTest.manifest.xml"); + string outputCsPath = Path.Combine(OutputDir, "SimpleTestParser.cs"); + + Output.WriteLine($"Manifest: {manifestPath}"); + Output.WriteLine($"Output: {outputCsPath}"); + + Assert.True(File.Exists(manifestPath), $"Manifest file not found: {manifestPath}"); + + // Act - Step 1: Run TraceParserGen.exe + string traceParserGenPath = GetTraceParserGenExePath(); + Output.WriteLine($"TraceParserGen.exe: {traceParserGenPath}"); + + var exitCode = RunTraceParserGen(traceParserGenPath, manifestPath, outputCsPath); + + // Assert - Step 1: Verify TraceParserGen succeeded + Assert.Equal(0, exitCode); + Assert.True(File.Exists(outputCsPath), $"Generated C# file not found: {outputCsPath}"); + + // Verify the generated file has expected content + string generatedContent = File.ReadAllText(outputCsPath); + Assert.Contains("class", generatedContent); + Assert.Contains("TraceEventParser", generatedContent); + + Output.WriteLine("Successfully generated parser from manifest"); + + // Act - Step 2: Create and build a test console application + string testProjectDir = Path.Combine(OutputDir, "TestApp"); + Directory.CreateDirectory(testProjectDir); + + CreateTestConsoleApp(testProjectDir, outputCsPath); + + // Act - Step 3: Build the test application + var buildExitCode = BuildTestApp(testProjectDir); + Assert.Equal(0, buildExitCode); + + // Act - Step 4: Run the test application + var runExitCode = RunTestApp(testProjectDir); + + // Assert - Step 4: Verify test app ran successfully (no crashes, no asserts) + Assert.Equal(0, runExitCode); + + Output.WriteLine("Test completed successfully"); + } + + private int RunTraceParserGen(string exePath, string manifestPath, string outputPath) + { + var startInfo = new ProcessStartInfo + { + FileName = exePath, + Arguments = $"\"{manifestPath}\" \"{outputPath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + Output.WriteLine($"Running: {startInfo.FileName} {startInfo.Arguments}"); + + using (var process = Process.Start(startInfo)) + { + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + if (!string.IsNullOrWhiteSpace(output)) + { + Output.WriteLine("STDOUT:"); + Output.WriteLine(output); + } + + if (!string.IsNullOrWhiteSpace(error)) + { + Output.WriteLine("STDERR:"); + Output.WriteLine(error); + } + + return process.ExitCode; + } + } + + private void CreateTestConsoleApp(string projectDir, string generatedParserPath) + { + // Find the TraceEvent.csproj relative to the test assembly location + // The test runs from bin\Release\net462, so we go up to src and then to TraceEvent + string testAssemblyDir = Environment.CurrentDirectory; + string srcDir = Path.GetFullPath(Path.Combine(testAssemblyDir, "..", "..", "..", "..")); + string traceEventProjectPath = Path.Combine(srcDir, "TraceEvent", "TraceEvent.csproj"); + + // Verify the path exists + if (!File.Exists(traceEventProjectPath)) + { + throw new FileNotFoundException($"Could not find TraceEvent.csproj at {traceEventProjectPath}"); + } + + // Create the .csproj file with ProjectReference + string csprojContent = $@" + + Exe + net462 + true + + + + +"; + + File.WriteAllText(Path.Combine(projectDir, "TestApp.csproj"), csprojContent); + + // Copy the generated parser file + string destParserPath = Path.Combine(projectDir, Path.GetFileName(generatedParserPath)); + File.Copy(generatedParserPath, destParserPath, true); + + // Create a simple trace file to use for testing + // We'll use one of the existing test trace files + string sampleTracePath = Path.Combine(TestDataDir, "..", "..", "TraceEvent", "TraceEvent.Tests", "inputs", "net.4.5.x86.etl.zip"); + + // Create Program.cs that uses the generated parser with a real trace file + string programContent = $@"using System; +using System.Linq; +using System.Reflection; +using Microsoft.Diagnostics.Tracing; + +class Program +{{ + static int Main(string[] args) + {{ + try + {{ + Console.WriteLine(""Starting parser test...""); + + // Find all TraceEventParser-derived types in the current assembly + var assembly = Assembly.GetExecutingAssembly(); + var parserTypes = assembly.GetTypes() + .Where(t => typeof(TraceEventParser).IsAssignableFrom(t) && !t.IsAbstract) + .ToList(); + + Console.WriteLine($""Found {{parserTypes.Count}} parser type(s)""); + + if (parserTypes.Count == 0) + {{ + Console.WriteLine(""ERROR: No parser types found""); + return 1; + }} + + // Get trace file path (from args or use default) + string traceFilePath = args.Length > 0 ? args[0] : ""{sampleTracePath.Replace("\\", "\\\\")}""; + + if (!System.IO.File.Exists(traceFilePath)) + {{ + Console.WriteLine($""ERROR: Trace file not found: {{traceFilePath}}""); + return 1; + }} + + Console.WriteLine($""Using trace file: {{traceFilePath}}""); + + using (var source = TraceEventDispatcher.GetDispatcherFromFileName(traceFilePath)) + {{ + foreach (var parserType in parserTypes) + {{ + Console.WriteLine($"" Testing parser: {{parserType.Name}}""); + + // Create an instance of the parser + var parser = (TraceEventParser)Activator.CreateInstance(parserType, source); + + int eventCount = 0; + + // Hook the All event to count events processed by this parser + parser.All += (TraceEvent data) => + {{ + eventCount++; + }}; + + // Process the trace (this will trigger events if any match) + source.Process(); + + Console.WriteLine($"" Processed {{eventCount}} event(s) from this parser""); + }} + }} + + Console.WriteLine(""Parser test completed successfully""); + return 0; + }} + catch (Exception ex) + {{ + Console.WriteLine($""ERROR: {{ex.Message}}""); + Console.WriteLine(ex.StackTrace); + return 1; + }} + }} +}}"; + + File.WriteAllText(Path.Combine(projectDir, "Program.cs"), programContent); + } + + private int BuildTestApp(string projectDir) + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -c Release", + WorkingDirectory = projectDir, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + Output.WriteLine($"Building test app in: {projectDir}"); + + using (var process = Process.Start(startInfo)) + { + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + if (!string.IsNullOrWhiteSpace(output)) + { + Output.WriteLine("Build STDOUT:"); + Output.WriteLine(output); + } + + if (!string.IsNullOrWhiteSpace(error)) + { + Output.WriteLine("Build STDERR:"); + Output.WriteLine(error); + } + + return process.ExitCode; + } + } + + private int RunTestApp(string projectDir) + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "run -c Release", + WorkingDirectory = projectDir, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + Output.WriteLine($"Running test app in: {projectDir}"); + + using (var process = Process.Start(startInfo)) + { + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + if (!string.IsNullOrWhiteSpace(output)) + { + Output.WriteLine("Run STDOUT:"); + Output.WriteLine(output); + } + + if (!string.IsNullOrWhiteSpace(error)) + { + Output.WriteLine("Run STDERR:"); + Output.WriteLine(error); + } + + return process.ExitCode; + } + } + } +} diff --git a/src/TraceParserGen.Tests/README.md b/src/TraceParserGen.Tests/README.md new file mode 100644 index 000000000..147c326fa --- /dev/null +++ b/src/TraceParserGen.Tests/README.md @@ -0,0 +1,85 @@ +# TraceParserGen.Tests + +This project contains automated tests for the TraceParserGen tool, which generates C# parser classes from ETW manifests or EventSource implementations. + +## Overview + +TraceParserGen.Tests implements a comprehensive test framework that validates the entire code generation pipeline: + +1. **Run TraceParserGen.exe** with test input (manifest file or EventSource DLL) +2. **Verify successful generation** of C# parser files +3. **Create a temporary console project** that references TraceEvent and includes the generated parser +4. **Build the temporary project** to ensure the generated code compiles +5. **Run the test application** to verify no runtime errors or assertions occur + +## Test Structure + +### Test Files + +- **ParserGenerationTests.cs**: Main test class containing test cases for parser generation +- **TestBase.cs**: Base class providing common test infrastructure and helper methods +- **inputs/**: Directory containing test input files (manifests, sample DLLs) + +### Sample Test + +The `CanGenerateParserFromManifest` test demonstrates the full pipeline: +- Uses a simple ETW manifest (`SimpleTest.manifest.xml`) as input +- Generates a parser class from the manifest +- Creates a temporary console app that uses reflection to find and instantiate the parser +- Builds and runs the console app to ensure everything works + +## Requirements + +- **Windows**: Tests require Windows with .NET Framework support to run TraceParserGen.exe +- **TraceParserGen**: The TraceParserGen project must be built before running tests +- **TraceEvent**: The TraceEvent project must be built before running tests + +## Running Tests + +```bash +# Build dependencies first +dotnet build src/TraceParserGen/TraceParserGen.csproj -c Release +dotnet build src/TraceEvent/TraceEvent.csproj -c Release + +# Run tests +dotnet test src/TraceParserGen.Tests/TraceParserGen.Tests.csproj -c Release +``` + +## Adding New Tests + +To add a new test case: + +1. Add your test input file (manifest or DLL) to the `inputs/` directory +2. Create a new test method in `ParserGenerationTests.cs` +3. Follow the pattern of the existing `CanGenerateParserFromManifest` test: + - Call `RunTraceParserGen()` to generate the parser + - Call `CreateTestConsoleApp()` to create a test application + - Call `BuildTestApp()` to build the test application + - Call `RunTestApp()` to verify the generated code works + +## Test Console Application + +The test creates a temporary console application that: +- References the Microsoft.Diagnostics.Tracing.TraceEvent library +- Includes the generated parser C# file +- Uses reflection to discover all TraceEventParser-derived types +- Verifies the parsers can be instantiated and have the expected methods + +This approach allows us to test that the generated code: +- Compiles successfully +- Contains valid C# syntax +- Implements the expected TraceEventParser interface +- Can be used in a real application + +## Platform Notes + +Tests are designed to run on Windows where TraceParserGen.exe (a .NET Framework application) can run natively. On non-Windows platforms, tests will skip with an informational message. + +## Future Enhancements + +Potential improvements to the test framework: +- Add tests for EventSource-based parser generation (using DLLs as input) +- Add tests for complex manifest scenarios (multiple providers, complex templates) +- Add validation of generated parser output against expected baselines +- Add performance benchmarks for parser generation +- Add tests that actually parse ETL files with the generated parsers diff --git a/src/TraceParserGen.Tests/TestBase.cs b/src/TraceParserGen.Tests/TestBase.cs new file mode 100644 index 000000000..e78272066 --- /dev/null +++ b/src/TraceParserGen.Tests/TestBase.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using Xunit.Abstractions; + +namespace TraceParserGen.Tests +{ + /// + /// Base class for TraceParserGen tests + /// + public abstract class TestBase + { + protected static string TestDataDir = Path.Combine(Environment.CurrentDirectory, "inputs"); + + protected TestBase(ITestOutputHelper output) + { + Output = output; + OutputDir = Path.Combine(Path.GetTempPath(), "TraceParserGen.Tests", Guid.NewGuid().ToString("N").Substring(0, 8)); + + Directory.CreateDirectory(OutputDir); + } + + protected ITestOutputHelper Output { get; } + + protected string OutputDir { get; } + + /// + /// Gets the path to the TraceParserGen.exe executable + /// + protected string GetTraceParserGenExePath() + { + // TraceParserGen.exe is copied to the output directory during build + string exePath = Path.Combine(Environment.CurrentDirectory, "TraceParserGen.exe"); + + if (!File.Exists(exePath)) + { + throw new FileNotFoundException($"Could not find TraceParserGen.exe at {exePath}. Please build the TraceParserGen.Tests project."); + } + + return exePath; + } + + } +} diff --git a/src/TraceParserGen.Tests/TraceParserGen.Tests.csproj b/src/TraceParserGen.Tests/TraceParserGen.Tests.csproj new file mode 100644 index 000000000..38950003c --- /dev/null +++ b/src/TraceParserGen.Tests/TraceParserGen.Tests.csproj @@ -0,0 +1,50 @@ + + + + + net462 + TraceParserGen.Tests + TraceParserGen.Tests + Unit tests for TraceParserGen. + Copyright © Microsoft 2024 + + + true + true + + + + + + + + + + + false + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + True + + + diff --git a/src/TraceParserGen.Tests/inputs/SimpleTest.manifest.xml b/src/TraceParserGen.Tests/inputs/SimpleTest.manifest.xml new file mode 100644 index 000000000..cf73a47d8 --- /dev/null +++ b/src/TraceParserGen.Tests/inputs/SimpleTest.manifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +