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