Skip to content

Commit 4b55e79

Browse files
authored
Updates to support 'low code' integration with SDK scripts. (#668)
1 parent 9da3e81 commit 4b55e79

65 files changed

Lines changed: 2264 additions & 1005 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.0.5
1+
3.0.6

src/VirtualClient/VirtualClient.Actions/SPECjbb/SpecJbbExecutor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ private string CalculateJavaOptions()
236236
return jbbArgument;
237237
}
238238

239-
private Task UploadGcLogAsync(IBlobManager blobManager, string gcLogPath, DateTime logTime, CancellationToken cancellationToken)
239+
private async Task UploadGcLogAsync(IBlobManager blobManager, string gcLogPath, DateTime logTime, CancellationToken cancellationToken)
240240
{
241241
// Example Blob Store Structure:
242242
// /7dfae74c-06c0-49fc-ade6-987534bb5169/anyagentid/specjbb/2022-04-30T20:13:23.3768938Z-gc.log
@@ -252,8 +252,8 @@ private Task UploadGcLogAsync(IBlobManager blobManager, string gcLogPath, DateTi
252252
null,
253253
this.Roles?.FirstOrDefault()));
254254

255-
return this.UploadFileAsync(blobManager, this.fileSystem, descriptor, cancellationToken, deleteFile: true);
256-
255+
await this.UploadFileAsync(blobManager, this.fileSystem, descriptor, cancellationToken);
256+
await this.fileSystem.File.DeleteAsync(gcLogPath);
257257
}
258258

259259
private async Task ValidateProcessExitedAsync(int processId, TimeSpan timeout, CancellationToken cancellationToken)

src/VirtualClient/VirtualClient.Actions/SPECview/SpecViewExecutor.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ private async Task SetUpEnvironmentVariable()
267267
this.SetEnvironmentVariable(EnvironmentVariable.PATH, visualStudioCRuntimeDllPath, EnvironmentVariableTarget.Machine, append: true);
268268
}
269269

270-
private Task UploadSpecviewLogAsync(IBlobManager blobManager, string specviewLogPath, DateTime logTime, CancellationToken cancellationToken)
270+
private async Task UploadSpecviewLogAsync(IBlobManager blobManager, string specviewLogPath, DateTime logTime, CancellationToken cancellationToken)
271271
{
272272
// Example Blob Store Structure:
273273
// 9ed58814-435b-4900-8eb2-af86393e0059/my-vc/specview/specviewperf/2023-10-11T22-07-41-73235Z-log.txt
@@ -282,7 +282,9 @@ private Task UploadSpecviewLogAsync(IBlobManager blobManager, string specviewLog
282282
this.Scenario,
283283
null,
284284
this.Roles?.FirstOrDefault()));
285-
return this.UploadFileAsync(blobManager, this.fileSystem, descriptor, cancellationToken, deleteFile: false);
285+
286+
await this.UploadFileAsync(blobManager, this.fileSystem, descriptor, cancellationToken);
287+
await this.fileSystem.File.DeleteAsync(specviewLogPath);
286288
}
287289
}
288290
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace VirtualClient.Actions
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.IO.Abstractions;
10+
using System.Linq;
11+
using System.Text;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
using Microsoft.Extensions.DependencyInjection;
15+
using MimeMapping;
16+
using Polly;
17+
using VirtualClient.Common;
18+
using VirtualClient.Common.Extensions;
19+
using VirtualClient.Common.Telemetry;
20+
using VirtualClient.Contracts;
21+
using VirtualClient.Contracts.Metadata;
22+
23+
/// <summary>
24+
/// A generic command executor with file upload and metrics upload support.
25+
/// </summary>
26+
[SupportedPlatforms("linux-arm64,linux-x64,win-arm64,win-x64")]
27+
public class CommandExecutor : ExecuteCommand
28+
{
29+
private IFileSystem fileSystem;
30+
private ISystemManagement systemManagement;
31+
32+
/// <summary>
33+
/// Constructor for <see cref="ScriptExecutor"/>
34+
/// </summary>
35+
/// <param name="dependencies">Provides required dependencies to the component.</param>
36+
/// <param name="parameters">Parameters defined in the profile or supplied on the command line.</param>
37+
public CommandExecutor(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters)
38+
: base(dependencies, parameters)
39+
{
40+
this.systemManagement = this.Dependencies.GetService<ISystemManagement>();
41+
this.fileSystem = this.systemManagement.FileSystem;
42+
}
43+
44+
/// <summary>
45+
/// The Regex Based, semi-colon separated relative log file/Folder Paths
46+
/// </summary>
47+
public IEnumerable<string> LogPaths
48+
{
49+
get
50+
{
51+
this.Parameters.TryGetCollection<string>(nameof(this.LogPaths), out IEnumerable<string> logPaths);
52+
return logPaths;
53+
}
54+
}
55+
56+
/// <summary>
57+
/// The ToolName for better logging and metadata
58+
/// </summary>
59+
public string ToolName
60+
{
61+
get
62+
{
63+
return this.Parameters.GetValue<string>(nameof(this.ToolName), string.Empty);
64+
}
65+
66+
set
67+
{
68+
this.Parameters[nameof(this.ToolName)] = value;
69+
}
70+
}
71+
72+
/// <summary>
73+
/// A retry policy to apply to file access/move operations.
74+
/// </summary>
75+
protected IAsyncPolicy FileOperationsRetryPolicy { get; set; } = RetryPolicies.FileOperations;
76+
77+
/// <summary>
78+
/// Executes the workload.
79+
/// </summary>
80+
protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
81+
{
82+
DateTime startTime = DateTime.UtcNow;
83+
84+
try
85+
{
86+
await base.ExecuteAsync(telemetryContext, cancellationToken);
87+
}
88+
finally
89+
{
90+
DateTime endTime = DateTime.UtcNow;
91+
await this.CaptureMetricsAsync(startTime, endTime, telemetryContext, cancellationToken);
92+
await this.CaptureLogsAsync(cancellationToken);
93+
}
94+
}
95+
96+
/// <summary>
97+
/// Captures the workload logs and the Workload metrics.
98+
/// </summary>
99+
protected Task CaptureMetricsAsync(DateTime startTime, DateTime endTime, EventContext telemetryContext, CancellationToken cancellationToken)
100+
{
101+
this.MetadataContract.AddForScenario(
102+
this.Scenario,
103+
SensitiveData.ObscureSecrets(this.Command));
104+
105+
this.MetadataContract.Apply(telemetryContext);
106+
107+
bool metricsFileFound = false;
108+
109+
try
110+
{
111+
////if (this.fileSystem.File.Exists(this.MetricsFilePath))
112+
////{
113+
//// metricsFileFound = true;
114+
//// telemetryContext.AddContext("metricsFilePath", this.MetricsFilePath);
115+
//// string results = await this.fileSystem.File.ReadAllTextAsync(this.MetricsFilePath);
116+
117+
//// if (!string.IsNullOrWhiteSpace(results))
118+
//// {
119+
//// JsonMetricsParser parser = new JsonMetricsParser(results, this.Logger, telemetryContext);
120+
//// IList<Metric> workloadMetrics = parser.Parse();
121+
122+
//// this.Logger.LogMetrics(
123+
//// this.ToolName,
124+
//// (this.MetricScenario ?? this.Scenario) ?? "Script",
125+
//// process.StartTime,
126+
//// process.ExitTime,
127+
//// workloadMetrics,
128+
//// null,
129+
//// process.FullCommand(),
130+
//// this.Tags,
131+
//// telemetryContext);
132+
//// }
133+
////}
134+
}
135+
finally
136+
{
137+
telemetryContext.AddContext(nameof(metricsFileFound), metricsFileFound);
138+
}
139+
140+
return Task.CompletedTask;
141+
}
142+
143+
/// <summary>
144+
/// Captures the workload logs based on LogFiles parameter of ScriptExecutor.
145+
/// All the files inmatching sub-folders and all the matching files along with metrics file will be moved to the
146+
/// central Virtual Client logs directory.
147+
/// </summary>
148+
protected Task CaptureLogsAsync(CancellationToken cancellationToken)
149+
{
150+
// e.g.
151+
// /logs/anytool/executecustomscript1
152+
// /logs/anytool/executecustomscript2
153+
string destinitionLogsDir = this.Combine(this.GetLogDirectory(this.ToolName), DateTime.UtcNow.ToString("yyyy-MM-dd_hh-mm-ss"));
154+
155+
IEnumerable<string> files = this.fileSystem.Directory.EnumerateFiles(this.GetLogDirectory(), "*.*", SearchOption.AllDirectories);
156+
157+
////if (this.LogPaths?.Any() == true)
158+
////{
159+
//// foreach (string logPath in this.LogPaths)
160+
//// {
161+
//// if (string.IsNullOrWhiteSpace(logPath))
162+
//// {
163+
//// continue;
164+
//// }
165+
166+
//// string fullLogPath = this.fileSystem.Path.GetFullPath(this.fileSystem.Path.Combine(this.ExecutableDirectory, logPath));
167+
168+
//// // Check for Matching Sub-Directories
169+
//// if (this.fileSystem.Directory.Exists(fullLogPath))
170+
//// {
171+
//// foreach (string logFilePath in this.fileSystem.Directory.GetFiles(fullLogPath, "*", SearchOption.AllDirectories))
172+
//// {
173+
//// var logs = await this.MoveLogsAsync(logFilePath, destinitionLogsDir, cancellationToken, sourceRootDirectory: fullLogPath);
174+
//// await this.RequestLogUploadsAsync(logs);
175+
//// }
176+
//// }
177+
178+
//// // Check for Matching FileNames
179+
//// foreach (string logFilePath in this.fileSystem.Directory.GetFiles(this.ExecutableDirectory, logPath, SearchOption.AllDirectories))
180+
//// {
181+
//// var logs = await this.MoveLogsAsync(logFilePath, destinitionLogsDir, cancellationToken);
182+
//// await this.RequestLogUploadsAsync(logs);
183+
//// }
184+
//// }
185+
////}
186+
187+
////// Move test-metrics.json file if that exists
188+
////string metricsFilePath = this.Combine(this.ExecutableDirectory, "test-metrics.json");
189+
////if (this.fileSystem.File.Exists(metricsFilePath))
190+
////{
191+
//// var logs = await this.MoveLogsAsync(metricsFilePath, destinitionLogsDir, cancellationToken);
192+
//// await this.RequestLogUploadsAsync(logs);
193+
////}
194+
return Task.CompletedTask;
195+
}
196+
197+
/// <summary>
198+
/// Requests a file upload for each of the log file paths provided.
199+
/// </summary>
200+
protected async Task RequestLogUploadsAsync(IEnumerable<string> logPaths)
201+
{
202+
if (logPaths?.Any() == true && this.TryGetContentStoreManager(out IBlobManager blobManager))
203+
{
204+
foreach (string logPath in logPaths)
205+
{
206+
FileUploadDescriptor descriptor = this.CreateFileUploadDescriptor(
207+
new FileContext(
208+
this.fileSystem.FileInfo.New(logPath),
209+
MimeUtility.GetMimeMapping(logPath),
210+
Encoding.UTF8.WebName,
211+
this.ExperimentId,
212+
this.AgentId,
213+
this.ToolName,
214+
this.Scenario,
215+
null,
216+
this.Roles?.FirstOrDefault()));
217+
218+
await this.RequestFileUploadAsync(descriptor);
219+
}
220+
}
221+
}
222+
223+
/////// <summary>
224+
/////// Move the log files to central logs directory (retaining source directory structure) and Upload to Content Store.
225+
/////// </summary>
226+
////private async Task<IEnumerable<string>> MoveLogsAsync(string sourcePath, string destinationDirectory, CancellationToken cancellationToken, string sourceRootDirectory = null)
227+
////{
228+
//// List<string> targetLogs = new List<string>();
229+
//// if (!string.Equals(sourcePath, this.ExecutablePath))
230+
//// {
231+
//// if (!this.fileSystem.Directory.Exists(destinationDirectory))
232+
//// {
233+
//// this.fileSystem.Directory.CreateDirectory(destinationDirectory);
234+
//// }
235+
236+
//// string destPath = sourcePath;
237+
//// await (this.FileOperationsRetryPolicy ?? Policy.NoOpAsync()).ExecuteAsync(async () =>
238+
//// {
239+
//// await Task.Run(() =>
240+
//// {
241+
//// string fileName = Path.GetFileName(sourcePath);
242+
243+
//// if (!string.IsNullOrEmpty(sourceRootDirectory))
244+
//// {
245+
//// // Compute relative path from sourceRoot to sourcePath
246+
//// string relativePath = this.fileSystem.Path.GetRelativePath(sourceRootDirectory, sourcePath);
247+
//// string destDir = this.fileSystem.Path.Combine(destinationDirectory, this.fileSystem.Path.GetDirectoryName(relativePath));
248+
249+
//// if (!this.fileSystem.Directory.Exists(destDir))
250+
//// {
251+
//// this.fileSystem.Directory.CreateDirectory(destDir);
252+
//// }
253+
254+
//// destPath = this.Combine(destDir, fileName);
255+
//// }
256+
//// else
257+
//// {
258+
//// destPath = this.Combine(destinationDirectory, fileName);
259+
//// }
260+
261+
//// this.fileSystem.File.Move(sourcePath, destPath, true);
262+
//// targetLogs.Add(destPath);
263+
//// });
264+
//// });
265+
//// }
266+
267+
//// return targetLogs;
268+
////}
269+
}
270+
}

src/VirtualClient/VirtualClient.Contracts.UnitTests/ComponentFactoryTests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ public void ComponentFactorySetsComponentPropertiesToExpectedDefaults(string pro
8282
Assert.IsNotNull(component);
8383
Assert.IsNotNull(component.Parameters);
8484
Assert.IsFalse(component.LogToFile);
85-
Assert.IsTrue(component.LogTimestamped);
8685
Assert.IsNull(component.ContentPathTemplate);
8786
Assert.IsNull(component.LogFileName);
8887
Assert.IsNull(component.LogFolderName);
@@ -108,7 +107,6 @@ public void ComponentFactoryDoesNotInadvertentlyOverwriteComponentLevelPropertie
108107
Assert.IsNotNull(component);
109108
Assert.IsNotNull(component.Parameters);
110109
Assert.IsTrue(component.LogToFile);
111-
Assert.IsFalse(component.LogTimestamped);
112110
Assert.AreEqual("test.log", component.LogFileName);
113111
Assert.AreEqual("test", component.LogFolderName);
114112
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"Timestamp","ExperimentId","ExecutionSystem","ProfileName","ClientID","SeverityLevel","EventType","EventID","EventDescription","EventSource","EventInfo","AppHost","AppName","AppVersion","OperatingSystemPlatform","PlatformArchitecture","Metadata","Tags"
2+
"2026-03-24T23:45:54.920Z","9fc92c6b-3a2a-4e1c-8355-187380fcc1d4","","CRC-SDK","QUERIDA","5","SEL","crt_processor_ierr","","bmc.sel","Source=BMC;Sensor=#c1;Details=IERR 6f [00 ff ff];Type=Processor","QUERIDA","CRC-SDK-Demo-Scripts","1.0.1","Win32NT","win-x64",,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"Timestamp","ExperimentId","ExecutionSystem","ProfileName","ClientID","SeverityLevel","ToolName","ToolVersion","ScenarioName","ScenarioStartTime","ScenarioEndTime","MetricName","MetricValue","MetricDescription","MetricUnit","MetricCategorization","MetricRelativity","MetricVerbosity","AppHost","AppName","AppVersion","OperatingSystemPlatform","PlatformArchitecture","Metadata","ToolResults","Tags"
2+
"2026-03-24T23:45:53.822Z","9fc92c6b-3a2a-4e1c-8355-187380fcc1d4","","CRC-SDK","QUERIDA","2","Monitor_1","","PreTest","2026-03-24T23:45:53.682Z","2026-03-24T23:45:53.758Z","pass","1","Defines a 'pass' count for a particular toolset and scenario execution outcome.","count","","HigherIsBetter","1","QUERIDA","CRC-SDK-Demo-Scripts","1.0.1","Win32NT","win-x64",,"",
3+
"2026-03-24T23:45:54.931Z","9fc92c6b-3a2a-4e1c-8355-187380fcc1d4","","CRC-SDK","QUERIDA","2","Monitor_2","","PreTest","2026-03-24T23:45:54.855Z","2026-03-24T23:45:54.927Z","pass","1","Defines a 'pass' count for a particular toolset and scenario execution outcome.","count","","HigherIsBetter","1","QUERIDA","CRC-SDK-Demo-Scripts","1.0.1","Win32NT","win-x64",,"",
4+
"2026-03-24T23:45:56.013Z","9fc92c6b-3a2a-4e1c-8355-187380fcc1d4","","CRC-SDK","QUERIDA","2","Monitor_3","","PreTest","2026-03-24T23:45:55.953Z","2026-03-24T23:45:55.997Z","pass","1","Defines a 'pass' count for a particular toolset and scenario execution outcome.","count","","HigherIsBetter","1","QUERIDA","CRC-SDK-Demo-Scripts","1.0.1","Win32NT","win-x64",,"",
5+
"2026-03-24T23:45:57.318Z","9fc92c6b-3a2a-4e1c-8355-187380fcc1d4","","CRC-SDK","QUERIDA","2","Invoke-PreTest","","PreTest","2026-03-24T23:45:52.766Z","2026-03-24T23:45:57.316Z","pass","1","Defines a 'pass' count for a particular toolset and scenario execution outcome.","count","","HigherIsBetter","1","QUERIDA","CRC-SDK-Demo-Scripts","1.0.1","Win32NT","win-x64",,"",
6+
"2026-03-24T23:45:57.330Z","9fc92c6b-3a2a-4e1c-8355-187380fcc1d4","","CRC-SDK","QUERIDA","2","Invoke-PreTest","","PreTest","2026-03-24T23:45:52.766Z","2026-03-24T23:45:57.316Z","elapsed_time","4.5498413","The time elapsed (in seconds) to complete the operation.","seconds","","LowerIsBetter","1","QUERIDA","CRC-SDK-Demo-Scripts","1.0.1","Win32NT","win-x64",,"",

src/VirtualClient/VirtualClient.Contracts.UnitTests/Extensibility/DelimitedEventDataEnumeratorTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ public void DelimitedEventDataEnumeratorParsesExpectedEventsFromFilesInQuotedCsv
7676
}
7777
}
7878

79+
[Test]
80+
public void DelimitedEventDataEnumeratorParsesExpectedEventsFromFilesInQuotedCsvFormat_Sdk_Scenario()
81+
{
82+
this.SetupTest(PlatformID.Unix);
83+
84+
// Scenario:
85+
// This test is designed to evaluate parsing logic from an actual file produced by the
86+
// SDK (e.g. Export-EventCsv).
87+
88+
string csvContent = System.IO.File.ReadAllText(this.Combine(DelimitedEventDataEnumeratorTests.Examples, "csv_for_sdk.events"));
89+
List<EventDataPoint> dataPoints = new List<EventDataPoint>();
90+
91+
Assert.DoesNotThrow(() =>
92+
{
93+
using (var enumerator = new DelimitedEventDataEnumerator(csvContent, DataFormat.Csv))
94+
{
95+
while (enumerator.MoveNext())
96+
{
97+
dataPoints.Add(enumerator.Current);
98+
}
99+
}
100+
});
101+
102+
Assert.AreEqual(1, dataPoints.Count);
103+
}
104+
79105
[Test]
80106
public void DelimitedEventDataEnumeratorParsesExpectedEventsFromFilesInJsonFormat()
81107
{

0 commit comments

Comments
 (0)