239 lines
6.5 KiB
C#
239 lines
6.5 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
using TinyCC.Core;
|
|
|
|
namespace TinyCC.E2ETests;
|
|
|
|
/// <summary>
|
|
/// 端到端测试运行器
|
|
/// 负责编译 C 源代码并执行生成的 ELF 文件
|
|
/// </summary>
|
|
public sealed class E2ETestRunner
|
|
{
|
|
private readonly string _tempDirectory;
|
|
|
|
public E2ETestRunner(string? tempDirectory = null)
|
|
{
|
|
_tempDirectory = tempDirectory ?? "/tmp/tinycc-debug"; // 固定路径便于调试
|
|
Directory.CreateDirectory(_tempDirectory);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 运行单个测试用例
|
|
/// </summary>
|
|
public async Task<TestResult> RunTestAsync(TestCase testCase)
|
|
{
|
|
var sourceFile = Path.Combine(_tempDirectory, $"{testCase.Name}.c");
|
|
var outputFile = Path.Combine(_tempDirectory, $"{testCase.Name}.out");
|
|
|
|
try
|
|
{
|
|
// 写入源代码
|
|
await File.WriteAllTextAsync(sourceFile, testCase.SourceCode);
|
|
|
|
// 编译
|
|
var compileResult = Compile(sourceFile, outputFile);
|
|
if (!compileResult.Success)
|
|
{
|
|
return new TestResult(
|
|
testCase.Name,
|
|
false,
|
|
-1,
|
|
null,
|
|
$"编译失败: {compileResult.Error}"
|
|
);
|
|
}
|
|
|
|
// 执行
|
|
var executeResult = await ExecuteAsync(outputFile);
|
|
|
|
// 调试:如果失败,打印 objdump
|
|
if (executeResult.ExitCode != testCase.ExpectedExitCode)
|
|
{
|
|
try
|
|
{
|
|
var psi = new ProcessStartInfo("objdump", $"-d -M intel \"{outputFile}\"")
|
|
{
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false
|
|
};
|
|
using var p = Process.Start(psi)!;
|
|
var dump = await p.StandardOutput.ReadToEndAsync();
|
|
Console.WriteLine($"[DEBUG] objdump:\n{dump}");
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// 验证结果
|
|
bool passed = executeResult.ExitCode == testCase.ExpectedExitCode;
|
|
if (testCase.ExpectedOutput != null && executeResult.Output != testCase.ExpectedOutput)
|
|
{
|
|
passed = false;
|
|
}
|
|
|
|
return new TestResult(
|
|
testCase.Name,
|
|
passed,
|
|
executeResult.ExitCode,
|
|
executeResult.Output,
|
|
passed ? null : $"期望退出码 {testCase.ExpectedExitCode},实际 {executeResult.ExitCode}"
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new TestResult(
|
|
testCase.Name,
|
|
false,
|
|
-1,
|
|
null,
|
|
$"测试执行异常: {ex.Message}"
|
|
);
|
|
}
|
|
}
|
|
|
|
private CompilationResult Compile(string sourceFile, string outputFile)
|
|
{
|
|
try
|
|
{
|
|
var errorReporter = new ErrorReporter();
|
|
var driver = new CompilerDriver(errorReporter);
|
|
|
|
var options = new CompilationOptions(
|
|
SourceFile: sourceFile,
|
|
OutputFile: outputFile,
|
|
Platform: TargetPlatform.LinuxX64
|
|
);
|
|
|
|
var result = driver.Compile(options);
|
|
|
|
if (errorReporter.HasErrors)
|
|
{
|
|
var errors = string.Join("\n", errorReporter.GetErrors().Select(e => e.ToString()));
|
|
return new CompilationResult(false, errors);
|
|
}
|
|
|
|
if (!result.Success)
|
|
{
|
|
return new CompilationResult(false, result.Message);
|
|
}
|
|
|
|
return new CompilationResult(true, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new CompilationResult(false, ex.Message);
|
|
}
|
|
}
|
|
|
|
private async Task<ExecutionResult> ExecuteAsync(string executablePath)
|
|
{
|
|
try
|
|
{
|
|
// 设置执行权限
|
|
var chmod = Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = "chmod",
|
|
Arguments = $"+x \"{executablePath}\"",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false
|
|
});
|
|
|
|
if (chmod != null)
|
|
{
|
|
await chmod.WaitForExitAsync();
|
|
}
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = executablePath,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using var process = Process.Start(psi)
|
|
?? throw new InvalidOperationException($"无法启动进程: {executablePath}");
|
|
|
|
var outputBuilder = new StringBuilder();
|
|
var errorBuilder = new StringBuilder();
|
|
|
|
process.OutputDataReceived += (sender, e) =>
|
|
{
|
|
if (e.Data != null)
|
|
{
|
|
outputBuilder.AppendLine(e.Data);
|
|
}
|
|
};
|
|
|
|
process.ErrorDataReceived += (sender, e) =>
|
|
{
|
|
if (e.Data != null)
|
|
{
|
|
errorBuilder.AppendLine(e.Data);
|
|
}
|
|
};
|
|
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
|
|
await process.WaitForExitAsync();
|
|
|
|
var output = outputBuilder.ToString().TrimEnd();
|
|
var error = errorBuilder.ToString().TrimEnd();
|
|
|
|
return new ExecutionResult(
|
|
process.ExitCode,
|
|
string.IsNullOrEmpty(output) ? error : output
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ExecutionResult(-1, $"执行失败: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public void Cleanup()
|
|
{
|
|
// 调试期间保留文件
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 测试用例
|
|
/// </summary>
|
|
public record TestCase(
|
|
string Name,
|
|
string SourceCode,
|
|
int ExpectedExitCode,
|
|
string? ExpectedOutput = null
|
|
);
|
|
|
|
/// <summary>
|
|
/// 测试结果
|
|
/// </summary>
|
|
public record TestResult(
|
|
string TestCaseName,
|
|
bool Passed,
|
|
int ActualExitCode,
|
|
string? ActualOutput,
|
|
string? ErrorMessage
|
|
);
|
|
|
|
/// <summary>
|
|
/// 编译结果
|
|
/// </summary>
|
|
public record CompilationResult(
|
|
bool Success,
|
|
string? Error
|
|
);
|
|
|
|
/// <summary>
|
|
/// 执行结果
|
|
/// </summary>
|
|
public record ExecutionResult(
|
|
int ExitCode,
|
|
string Output
|
|
);
|