forked from Azure/azure-sdk-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWorkspace.cs
More file actions
290 lines (258 loc) · 10.7 KB
/
Workspace.cs
File metadata and controls
290 lines (258 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using Azure.Sdk.Tools.Cli.Benchmarks.Models;
namespace Azure.Sdk.Tools.Cli.Benchmarks.Infrastructure;
/// <summary>
/// Represents a workspace containing a cloned repository for benchmark execution.
/// Provides file and git operations for interacting with the repository.
/// </summary>
public class Workspace : IDisposable
{
/// <summary>
/// Gets the path to the workspace root directory.
/// </summary>
public string RootPath { get; }
/// <summary>
/// Gets the path to the home repository within the workspace.
/// </summary>
public string RepoPath { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Workspace"/> class.
/// </summary>
/// <param name="rootPath">The path to the workspace root directory.</param>
/// <param name="repoName">The name of the repository directory within the workspace.</param>
public Workspace(string rootPath, string repoName)
{
RootPath = rootPath;
RepoPath = Path.Combine(rootPath, repoName);
}
/// <summary>
/// Gets the git diff of all uncommitted changes in the repository.
/// </summary>
/// <param name="contextLines">Number of context lines to include around each change (default: 3).</param>
/// <param name="includeUntracked">Whether to include untracked (newly created) files in the diff (default: true).</param>
/// <returns>The git diff output as a string.</returns>
public async Task<string> GetGitDiffAsync(int contextLines = 3, bool includeUntracked = true)
{
if (includeUntracked)
{
// Stage intent-to-add for all untracked files so they appear in the diff.
// This doesn't actually stage file contents, just makes git aware of them.
await RunGitCommandAsync("add", "--intent-to-add", ".");
}
return await RunGitCommandAsync("diff", $"-U{contextLines}");
}
/// <summary>
/// Runs a command in the repository directory.
/// </summary>
/// <param name="command">The command to run (e.g., "npm", "dotnet").</param>
/// <param name="args">Arguments to pass to the command.</param>
/// <returns>The standard output of the command.</returns>
/// <exception cref="InvalidOperationException">Thrown when the command exits with a non-zero exit code.</exception>
public async Task<string> RunCommandAsync(string command, params string[] args)
{
var startInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = RepoPath
};
// On Windows, wrap with cmd /c to resolve .cmd/.bat files (e.g., npm, tsp)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
startInfo.FileName = "cmd.exe";
startInfo.ArgumentList.Add("/c");
startInfo.ArgumentList.Add(command);
}
else
{
startInfo.FileName = command;
}
foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}
using var process = new Process { StartInfo = startInfo };
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"Command '{command} {string.Join(" ", args)}' failed with exit code {process.ExitCode}.\nStderr: {error}");
}
return output;
}
/// <summary>
/// Runs a git command in the repository directory.
/// </summary>
private async Task<string> RunGitCommandAsync(params string[] args)
{
var startInfo = new ProcessStartInfo
{
FileName = "git",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = RepoPath
};
foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}
using var process = new Process { StartInfo = startInfo };
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return output;
}
/// <summary>
/// Writes content to a file at the specified path relative to the repository root.
/// Creates parent directories if they don't exist.
/// </summary>
/// <param name="relativePath">The file path relative to the repository root.</param>
/// <param name="content">The content to write to the file.</param>
public async Task WriteFileAsync(string relativePath, string content)
{
var fullPath = Path.Combine(RepoPath, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await File.WriteAllTextAsync(fullPath, content);
}
/// <summary>
/// Reads the content of a file at the specified path relative to the repository root.
/// </summary>
/// <param name="relativePath">The file path relative to the repository root.</param>
/// <returns>The content of the file as a string.</returns>
public async Task<string> ReadFileAsync(string relativePath)
{
var fullPath = Path.Combine(RepoPath, relativePath);
return await File.ReadAllTextAsync(fullPath);
}
/// <summary>
/// Writes the benchmark execution log to the workspace root directory.
/// The log includes messages, tool calls, validation results, and other execution details.
/// </summary>
/// <param name="scenarioName">The name of the scenario that was executed.</param>
/// <param name="messages">The messages from the Copilot SDK session.</param>
/// <param name="toolCalls">The list of tool calls made during execution.</param>
/// <param name="gitDiff">The git diff of changes made during execution.</param>
/// <param name="duration">The duration of the execution.</param>
/// <param name="passed">Whether the benchmark passed validation.</param>
/// <param name="validation">The validation summary (null if no validators were run).</param>
/// <param name="error">Optional error message if the benchmark failed.</param>
public async Task WriteExecutionLogAsync(
string scenarioName,
IReadOnlyList<object> messages,
IReadOnlyList<ToolCallRecord> toolCalls,
string? gitDiff,
TimeSpan duration,
bool passed,
ValidationSummary? validation = null,
string? error = null)
{
var log = new
{
Scenario = scenarioName,
Timestamp = DateTime.UtcNow.ToString("o"),
Duration = duration.ToString(),
Passed = passed,
Error = error,
Validation = validation,
ToolCalls = toolCalls,
Messages = messages,
GitDiff = gitDiff
};
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(log, options);
var logPath = Path.Combine(RootPath, "benchmark-log.json");
await File.WriteAllTextAsync(logPath, json);
}
/// <summary>
/// Copies a file or directory from source to the workspace.
/// If the source is a directory, copies the entire directory recursively.
/// </summary>
/// <param name="sourcePath">The source file or directory path (absolute or relative to current directory).</param>
/// <param name="targetRelativePath">The target path relative to the repository root.</param>
public async Task CopyToWorkspaceAsync(string sourcePath, string targetRelativePath)
{
var targetPath = Path.Combine(RepoPath, targetRelativePath);
if (Directory.Exists(sourcePath))
{
// Copy entire directory
CopyDirectory(sourcePath, targetPath);
}
else if (File.Exists(sourcePath))
{
// Copy single file
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
await Task.Run(() => File.Copy(sourcePath, targetPath, overwrite: true));
}
else
{
throw new FileNotFoundException($"Source path not found: {sourcePath}");
}
}
/// <summary>
/// Removes a file or directory from the workspace.
/// If the path is a directory, removes the entire directory recursively.
/// </summary>
/// <param name="relativePath">The path relative to the repository root to remove.</param>
public async Task RemoveFromWorkspace(string relativePath)
{
var targetPath = Path.Combine(RepoPath, relativePath);
if (Directory.Exists(targetPath))
{
// Remove entire directory recursively
await Task.Run(() => Directory.Delete(targetPath, recursive: true));
}
else if (File.Exists(targetPath))
{
// Remove single file
await Task.Run(() => File.Delete(targetPath));
}
// If path doesn't exist, no-op (already removed)
}
/// <summary>
/// Recursively copies a directory and all its contents.
/// </summary>
/// <param name="sourceDir">The source directory path.</param>
/// <param name="targetDir">The target directory path.</param>
private static void CopyDirectory(string sourceDir, string targetDir)
{
// Create target directory
Directory.CreateDirectory(targetDir);
// Copy all files
foreach (var file in Directory.GetFiles(sourceDir))
{
var fileName = Path.GetFileName(file);
var targetFile = Path.Combine(targetDir, fileName);
File.Copy(file, targetFile, overwrite: true);
}
// Copy all subdirectories recursively
foreach (var subDir in Directory.GetDirectories(sourceDir))
{
var dirName = Path.GetFileName(subDir);
var targetSubDir = Path.Combine(targetDir, dirName);
CopyDirectory(subDir, targetSubDir);
}
}
/// <summary>
/// Disposes of the workspace resources.
/// Note: Cleanup of the workspace directory is handled by <see cref="WorkspaceManager"/> based on the configured cleanup policy.
/// </summary>
public void Dispose()
{
// Cleanup will be handled by WorkspaceManager based on CleanupPolicy
}
}