-
Notifications
You must be signed in to change notification settings - Fork 31
/
Example2WorkloadExecutorTests_MockFixture.cs
328 lines (295 loc) · 17.3 KB
/
Example2WorkloadExecutorTests_MockFixture.cs
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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace VirtualClient.Actions
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Polly;
using VirtualClient.Common.Extensions;
using VirtualClient.Common.Telemetry;
using VirtualClient.Contracts;
[TestFixture]
[Category("Unit")]
public class Example2WorkloadExecutorTests_MockFixture
{
private MockFixture fixture;
private DependencyPath mockWorkloadPackage;
private string validResults;
[Test]
public async Task ExampleWorkloadExecutorInstallsTheExpectedWorkloadPackageOnWindowsSystems()
{
this.SetupDefaultBehaviors(PlatformID.Win32NT);
using (TestExample2WorkloadExecutor executor = new TestExample2WorkloadExecutor(this.fixture))
{
// The following extension method usage illustrates how the use of an extension method
// (OnGetPackageLocation) can keep the code both readable and minimal for setting up mock
// behaviors.
this.fixture.PackageManager.OnGetPackage()
.Callback<string, CancellationToken>((packageName, token) => Assert.AreEqual(packageName, "SomeWorkload"))
.ReturnsAsync(this.mockWorkloadPackage);
await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
}
}
[Test]
public async Task ExampleWorkloadExecutorInstallsTheExpectedWorkloadPackageOnUnixSystems()
{
this.SetupDefaultBehaviors(PlatformID.Unix);
using (TestExample2WorkloadExecutor executor = new TestExample2WorkloadExecutor(this.fixture))
{
// The following extension method usage illustrates how the use of an extension method
// (OnGetPackageLocation) can keep the code both readable and minimal for setting up mock
// behaviors.
this.fixture.PackageManager.OnGetPackage()
.Callback<string, CancellationToken>((packageName, token) => Assert.AreEqual(packageName, "SomeWorkload"))
.ReturnsAsync(this.mockWorkloadPackage);
await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
}
}
[Test]
[TestCase(PlatformID.Win32NT, Architecture.X64)]
[TestCase(PlatformID.Win32NT, Architecture.Arm64)]
public async Task ExampleWorkloadExecutorVerifiesAndInitializesTheExpectedWorkloadPackageBinariesExistOnWindowsSystems(PlatformID platform, Architecture architecture)
{
this.SetupDefaultBehaviors(platform, architecture);
using (TestExample2WorkloadExecutor executor = new TestExample2WorkloadExecutor(this.fixture))
{
// The actual workload binaries will exist in the workload package in the "platform-specific" path.
// Each workload package can have binaries that support different platforms/architectures.
// (e.g. \packages\workload\1.0.0\win-x64\workload.exe, \packages\workload\1.0.0\win-arm64\workload.exe).
DependencyPath workloadPlatformSpecificPackage = this.fixture.ToPlatformSpecificPath(
this.mockWorkloadPackage,
platform,
architecture);
// The executor will verify the expected workload binaries exist on the file system.
List<string> expectedBinaries = new List<string>
{
this.fixture.Combine(workloadPlatformSpecificPackage.Path, "SomeWorkload.exe"),
this.fixture.Combine(workloadPlatformSpecificPackage.Path, "SomeTool1.exe"),
this.fixture.Combine(workloadPlatformSpecificPackage.Path, "SomeTool2.exe")
};
this.fixture.File.Setup(file => file.Exists(It.IsAny<string>()))
.Callback<string>(path => expectedBinaries.Remove(path)) // Remove the path as it is confirmed
.Returns(true);
await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
Assert.IsEmpty(expectedBinaries);
}
}
[Test]
[TestCase(PlatformID.Unix, Architecture.X64)]
[TestCase(PlatformID.Unix, Architecture.Arm64)]
public async Task ExampleWorkloadExecutorVerifiesAndInitializesTheExpectedWorkloadPackageBinariesExistOnLinuxSystems(PlatformID platform, Architecture architecture)
{
this.SetupDefaultBehaviors(platform, architecture);
using (TestExample2WorkloadExecutor executor = new TestExample2WorkloadExecutor(this.fixture))
{
// The actual workload binaries will exist in the workload package in the "platform-specific" path.
// Each workload package can have binaries that support different platforms/architectures.
// (e.g. \packages\workload\1.0.0\win-x64\workload.exe, \packages\workload\1.0.0\win-arm64\workload.exe).
DependencyPath workloadPlatformSpecificPackage = this.fixture.ToPlatformSpecificPath(
this.mockWorkloadPackage,
platform,
architecture);
// The executor will verify the expected workload binaries exist on the file system.
List<string> expectedBinaries = new List<string>
{
this.fixture.Combine(workloadPlatformSpecificPackage.Path, "SomeWorkload"),
this.fixture.Combine(workloadPlatformSpecificPackage.Path, "SomeTool1"),
this.fixture.Combine(workloadPlatformSpecificPackage.Path, "SomeTool2")
};
this.fixture.File.Setup(file => file.Exists(It.IsAny<string>()))
.Callback<string>(path => expectedBinaries.Remove(path)) // Remove the path as it is confirmed
.Returns(true);
await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
Assert.IsEmpty(expectedBinaries);
}
}
[Test]
public async Task ExampleWorkloadExecutorAppliesExpectedSystemSettingsOnFirstRun()
{
this.SetupDefaultBehaviors(PlatformID.Win32NT, Architecture.X64);
using (TestExample2WorkloadExecutor executor = new TestExample2WorkloadExecutor(this.fixture))
{
// Setup the scenario where a state object indicating the executor has run before
// a first time does not exist. This is how the executor determines that it has not
// performed a first run and thus that it needs to apply the system settings.
this.fixture.StateManager.OnGetState().ReturnsAsync(null as JObject);
await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
Assert.IsTrue(this.fixture.ProcessManager.Processes.Count() == 1);
Assert.IsNotNull(this.fixture.ProcessManager.Commands.FirstOrDefault(proc => proc.EndsWith("configureSystem.exe")));
}
}
[Test]
[TestCase(PlatformID.Win32NT, "SomeWorkload.exe Run")]
[TestCase(PlatformID.Unix, "SomeWorkload Run")]
public async Task ExampleWorkloadExecutorExecutesTheExpectedWorkloadCommand(PlatformID platform, string expectedExecutable)
{
this.SetupDefaultBehaviors(platform, Architecture.X64);
using (TestExample2WorkloadExecutor executor = new TestExample2WorkloadExecutor(this.fixture))
{
// The actual workload binaries will exist in the workload package in the "platform-specific" path.
// Each workload package can have binaries that support different platforms/architectures.
// (e.g. \packages\workload\1.0.0\win-x64\workload.exe, \packages\workload\1.0.0\win-arm64\workload.exe).
DependencyPath workloadPlatformSpecificPackage = this.fixture.ToPlatformSpecificPath(
this.mockWorkloadPackage,
platform,
Architecture.X64);
await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
string expectedWorkloadProcess = this.fixture.Combine(workloadPlatformSpecificPackage.Path, expectedExecutable);
Assert.IsTrue(this.fixture.ProcessManager.Commands.Contains(expectedWorkloadProcess));
}
}
private void SetupDefaultBehaviors(PlatformID platform, Architecture architecture = Architecture.X64)
{
// Test Setup Methodology:
// ----------------------------------------------------------------------------------------
// Setup all dependencies that are expected by the class under test that represent
// what would be the "happy path". This is the path where every dependency expected to
// exist and to be defined correctly actually exists. The class under test is expected to
// complete its operations successfully in this case. Then in individual tests, one of the
// dependency behaviors will be modified. This allows all different kinds of variations in
// potential behaviors to be tested simply and thoroughly.
//
// ** See the TESTING_README.md in the root of the solution directory. **
//
//
// What does the flow of the workload executor look like:
// ----------------------------------------------------------------------------------------
// The workload executor flow describes the ordered steps required to initialize, configure
// and execute the workload followed by capturing the metrics/results.
//
// 1) Install and initialize the workload package.
// The workload package contains the workload executables/binaries/scripts and any other
// files or content necessary to run the workload itself.
//
// 2) Check that all required workload binaries/scripts exist on the file system.
//
// 3) Ensure all required workload binaries/scripts are marked/attributed as executables (for Linux systems).
//
// 4) Save an initial state object mimicking workload executors that need to preserve state
// information (e.g. client/server interactions).
//
// 5) Execute the workload itself.
//
// 6) Capture/emit the workload results metrics.
//
//
// What are the dependencies:
// ----------------------------------------------------------------------------------------
// The ExampleWorkloadExecutor class has a number of different dependencies that must all be
// setup correctly in order to fully test the class.
//
// o File System Integration
// The file system integration dependency provides read/write access to the file system (e.g.
// directories, files).
//
// o Package Manager
// The package manager dependency provides the functionality for installing/downloading workload
// packages to the system as well as for finding their location.
//
// o State Manager
// The state manager dependency provides access to saving and retrieving state objects/definitions.
// Workload executors use state objects to maintain information over long periods of time in-between
// individual executions (where the information could be lost if maintained only in memory).
//
// o Process Manager
// The process manager dependency is used to create isolated processes on the system for execution
// of the workload executables and scripts.
//
//
// ...Setting up the Happy Path
// ----------------------------------------------------------------------------------------
// Setup the fixture itself to target the platform provided (e.g. Windows, Unix). This ensures the platform
// specifics are made relevant to that platform (e.g. file system paths, path structures).
this.fixture = new MockFixture();
this.fixture.Setup(platform, architecture);
// Expectation 1: The expected parameters are defined
string workloadName = "SomeWorkload";
this.fixture.Parameters.AddRange(new Dictionary<string, IConvertible>
{
{ nameof(Example2WorkloadExecutor.PackageName), workloadName },
{ nameof(Example2WorkloadExecutor.CommandLine), "Run" },
{ nameof(Example2WorkloadExecutor.TestName), "ExampleTest" }
});
// Expectation 2: The expected workload package actually exists.
this.mockWorkloadPackage = new DependencyPath(
workloadName,
this.fixture.PlatformSpecifics.GetPackagePath(workloadName));
this.fixture.PackageManager
.Setup(mgr => mgr.GetPackageAsync(workloadName, It.IsAny<CancellationToken>()))
.ReturnsAsync(this.mockWorkloadPackage);
// Expectation 3: The expected workload binaries/scripts within the workload package exist. Workload packages
// follow a strict schema allowing for toolset versions that support different platforms and architectures
// (e.g. linux-x64, linux-arm64, win-x64, win-arm64).
DependencyPath platformSpecificWorkloadPackage = this.fixture.ToPlatformSpecificPath(
this.mockWorkloadPackage,
platform,
architecture);
List<string> expectedBinaries = platform == PlatformID.Win32NT
? new List<string>
{
// Expected binaries on Windows
this.fixture.Combine(platformSpecificWorkloadPackage.Path, $"{workloadName}.exe"),
this.fixture.Combine(platformSpecificWorkloadPackage.Path, "SomeTool1.exe"),
this.fixture.Combine(platformSpecificWorkloadPackage.Path, "SomeTool2.exe")
}
: new List<string>
{
// Expected binaries on Linux/Unix
this.fixture.Combine(platformSpecificWorkloadPackage.Path, $"{workloadName}"),
this.fixture.Combine(platformSpecificWorkloadPackage.Path, "SomeTool1"),
this.fixture.Combine(platformSpecificWorkloadPackage.Path, "SomeTool2")
};
expectedBinaries.ForEach(binary => this.fixture.File.Setup(file => file.Exists(binary)).Returns(true));
// Expectation 4: The executor already executed once and applied expected settings. This requires a reboot.
// The executor uses a state object (typically saved on the file system) to pick back up where it left off
// once it reboots and is running again. By default we want to test the part of the code after this happens.
this.fixture.StateManager
.Setup(mgr => mgr.GetStateAsync(
$"{nameof(Example2WorkloadExecutor)}-state",
It.IsAny<CancellationToken>(),
It.IsAny<IAsyncPolicy>()))
.ReturnsAsync(JObject.FromObject(new Example2WorkloadExecutor.WorkloadState { IsFirstRun = false }));
// Expectation 5: The workload runs and produces valid results
this.validResults = "{ \"calculationsPerSec\": 123456789, \"avgLatency\": 98, \"score\": 450 }";
this.fixture.ProcessManager.OnProcessCreated = (process) =>
{
// e.g. ExampleWorkload.exe Run
if (process.FullCommand().Contains("Run"))
{
// Mimic the workload process having written valid results.
process.StandardOutput.Append(this.validResults);
}
};
}
// A private class inside of the unit test class is often used to enable the developer
// to expose protected members of the class under test. This is a simple technique to
// allow those methods to be tested directly.
private class TestExample2WorkloadExecutor : Example2WorkloadExecutor
{
public TestExample2WorkloadExecutor(MockFixture fixture)
: base(fixture.Dependencies, fixture.Parameters)
{
}
// Use the access modifier "public new" in order to expose the underlying protected method
// without overriding it. This technique is recommended ONLY for testing scenarios but is very
// helpful for those.
public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
return base.InitializeAsync(telemetryContext, cancellationToken);
}
}
}
}