Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions PerfView.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
288 changes: 288 additions & 0 deletions src/TraceParserGen.Tests/ParserGenerationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;
using Xunit.Abstractions;

namespace TraceParserGen.Tests
{
/// <summary>
/// Tests for TraceParserGen.exe that validate it can generate parsers from manifests
/// </summary>
public class ParserGenerationTests : TestBase
{
public ParserGenerationTests(ITestOutputHelper output) : base(output)
{
}

[Fact]
public void CanGenerateParserFromManifest()
{
// Skip on non-Windows platforms since TraceParserGen.exe is a .NET Framework app
// In a real environment, this would run on Windows with proper .NET Framework support
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Output.WriteLine("Skipping test on non-Windows platform. TraceParserGen.exe requires .NET Framework.");
return;
}

// 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)
{
ProcessStartInfo startInfo;

// On Linux/Mac, we need to use mono to run .NET Framework executables
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
startInfo = new ProcessStartInfo
{
FileName = exePath,
Arguments = $"\"{manifestPath}\" \"{outputPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
}
else
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests only run on Windows, so you can remove the else here and just unconditionally run the Windows path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the else block and mono-related code. Tests now only support Windows execution path. Commit: f5dd42c

{
startInfo = new ProcessStartInfo
{
FileName = "mono",
Arguments = $"\"{exePath}\" \"{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)
{
// Create the .csproj file
string csprojContent = $@"<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net462</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Reference Include=""Microsoft.Diagnostics.Tracing.TraceEvent"">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than using a <Reference> tag, this should be a <ProjectReference> tag pointing at TraceEvent.csproj in the repo. You'll need to implement this by using a relative path (e.g. ....\TraceEvent\TraceEvent.csproj). This might not be the right relative path, but you can tell by finding the source code at runtime. You'll need to do this because TraceEvent.dll won't be the only dependency needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use <ProjectReference> tag pointing to TraceEvent.csproj. The path is found at runtime by navigating from the test assembly location to the source directory. Commit: 0bc8f45

<HintPath>{GetTraceEventAssemblyPath()}</HintPath>
</Reference>
</ItemGroup>
</Project>";

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 Program.cs that uses reflection to instantiate parsers
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)"");

foreach (var parserType in parserTypes)
{
Console.WriteLine($"" Testing parser: {parserType.Name}"");

// Create an instance of the parser
// TraceEventParser constructors typically take a TraceEventSource parameter
// Since we don't have a real source, we'll just verify the type can be instantiated
// by checking if it has expected methods

var enumerateMethod = parserType.GetMethod(""EnumerateTemplates"",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of searching for EnumerateTemplates lets assume that the caller of this executable passed in the path to a trace file with events that will be parsed by the generated parser. You can create a TraceEventSource by calling TraceEventDispatcher.GetDispatcherFromFileName. Then you can make sure that the events show up properly in the parser by hooking the TraceEventParser.All on the generated parser.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the test application to use TraceEventDispatcher.GetDispatcherFromFileName with a trace file. The generated parser is now instantiated with the source, hooks TraceEventParser.All to count events, and processes the trace to validate the parser works correctly. Commit: f5dd42c

BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

if (enumerateMethod != null)
{
Console.WriteLine($"" Found EnumerateTemplates method"");
}
else
{
Console.WriteLine($"" WARNING: EnumerateTemplates method not found"");
}
}

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 --no-build",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove --no-build.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed --no-build flag. Commit: f5dd42c

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;
}
}
}
}
Loading