Skip to content

Commit 8157c5f

Browse files
authored
Add a couple of "stress tests" to the integration test project (#11481)
Two tests that run for an hour, adding and removing components and verifying that classification is correct afterwards. One uses an RCL, one doesn't. The tests are setup to not run in CI, only manually on dev machines, so as not to delay the CI runs, and because sometimes these have a different version of "success" :)
2 parents 579f384 + c1d7007 commit 8157c5f

13 files changed

+262
-19
lines changed

azure-pipelines-conditional-integration.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ variables:
1818
value: false
1919
- name: Codeql.SkipTaskAutoInjection
2020
value: true
21+
- name: _IntegrationTestsRunningInCI
22+
value: true
2123

2224
trigger: none
2325

azure-pipelines-integration-dartlab.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ variables:
3535
value: false
3636
- name: Codeql.SkipTaskAutoInjection
3737
value: true
38+
- name: _IntegrationTestsRunningInCI
39+
value: true
3840

3941
stages:
4042
- template: \stages\visual-studio\agent.yml@DartLabTemplates

azure-pipelines.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ variables:
2020
value: false
2121
- name: Codeql.SkipTaskAutoInjection
2222
value: true
23+
- name: _IntegrationTestsRunningInCI
24+
value: true
2325

2426
trigger:
2527
batch: true

src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/AbstractIntegrationTest.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public abstract class AbstractIntegrationTest : AbstractIdeIntegrationTest
3131
{
3232
protected CancellationToken ControlledHangMitigatingCancellationToken => HangMitigatingCancellationToken;
3333

34+
protected virtual bool AllowDebugFails => false;
35+
3436
public override async Task InitializeAsync()
3537
{
3638
// Not sure why the module initializer doesn't seem to work for integration tests
@@ -41,11 +43,14 @@ public override async Task InitializeAsync()
4143

4244
public override void Dispose()
4345
{
44-
var fails = ThrowingTraceListener.Fails;
45-
Assert.False(fails.Length > 0, $"""
46-
Expected 0 Debug.Fail calls. Actual:
47-
{string.Join(Environment.NewLine, fails)}
48-
""");
46+
if (!AllowDebugFails)
47+
{
48+
var fails = ThrowingTraceListener.Fails;
49+
Assert.False(fails.Length > 0, $"""
50+
Expected 0 Debug.Fail calls. Actual:
51+
{string.Join(Environment.NewLine, fails)}
52+
""");
53+
}
4954

5055
base.Dispose();
5156
}

src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/AbstractRazorEditorTest.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public abstract class AbstractRazorEditorTest(ITestOutputHelper testOutput) : Ab
3232

3333
protected virtual string TargetFrameworkElement => $"""<TargetFramework>{TargetFramework}</TargetFramework>""";
3434

35+
protected virtual string ProjectZipFile => "Microsoft.VisualStudio.Razor.IntegrationTests.TestFiles.BlazorProject.zip";
36+
37+
private protected virtual ILogger Logger => _testLogger.AssumeNotNull();
38+
3539
protected string ProjectFilePath => _projectFilePath.AssumeNotNull();
3640

3741
public override async Task InitializeAsync()
@@ -96,19 +100,20 @@ private async Task<string> CreateAndOpenBlazorProjectAsync(CancellationToken can
96100

97101
var solutionPath = CreateTemporaryPath();
98102

99-
var resourceName = "Microsoft.VisualStudio.Razor.IntegrationTests.TestFiles.BlazorProject.zip";
100-
using var zipStream = typeof(AbstractRazorEditorTest).Assembly.GetManifestResourceStream(resourceName);
103+
using var zipStream = typeof(AbstractRazorEditorTest).Assembly.GetManifestResourceStream(ProjectZipFile);
101104
using var zip = new ZipArchive(zipStream);
102105
zip.ExtractToDirectory(solutionPath);
103106

104107
var slnFile = Directory.EnumerateFiles(solutionPath, "*.sln").Single();
105-
var projectFile = Directory.EnumerateFiles(solutionPath, "*.csproj", SearchOption.AllDirectories).Single();
106108

107-
PrepareProjectForFirstOpen(projectFile);
109+
foreach (var projectFile in Directory.EnumerateFiles(solutionPath, "*.csproj", SearchOption.AllDirectories))
110+
{
111+
PrepareProjectForFirstOpen(projectFile);
112+
}
108113

109114
await TestServices.SolutionExplorer.OpenSolutionAsync(slnFile, cancellationToken);
110115

111-
return projectFile;
116+
return Directory.EnumerateFiles(solutionPath, $"{RazorProjectConstants.BlazorProjectName}.csproj", SearchOption.AllDirectories).Single();
112117
}
113118

114119
protected virtual void PrepareProjectForFirstOpen(string projectFileName)

src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Classification.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ internal partial class EditorInProcess
2020
/// </summary>
2121
/// <param name="cancellationToken">A cancellation token.</param>
2222
/// <param name="count">The number of the given classification to expect.</param>
23+
/// <param name="exact">Whether to wait for exactly <paramref name="count"/> classifications.</param>
2324
/// <returns>A <see cref="Task"/> which completes when classification is "ready".</returns>
24-
public Task WaitForComponentClassificationAsync(CancellationToken cancellationToken, int count = 1) => WaitForSemanticClassificationAsync("RazorComponentElement", cancellationToken, count);
25+
public Task WaitForComponentClassificationAsync(CancellationToken cancellationToken, int count = 1, bool exact = false)
26+
=> WaitForSemanticClassificationAsync("RazorComponentElement", cancellationToken, count, exact);
2527

2628
/// <summary>
2729
/// Waits for any semantic classifications to be available on the active TextView, and for at least one of the
@@ -30,8 +32,9 @@ internal partial class EditorInProcess
3032
/// <param name="cancellationToken">A cancellation token.</param>
3133
/// <param name="expectedClassification">The classification to wait for, if any.</param>
3234
/// <param name="count">The number of the given classification to expect.</param>
35+
/// <param name="exact">Whether to wait for exactly <paramref name="count"/> classifications.</param>
3336
/// <returns>A <see cref="Task"/> which completes when classification is "ready".</returns>
34-
public async Task WaitForSemanticClassificationAsync(string expectedClassification, CancellationToken cancellationToken, int count = 1)
37+
public async Task WaitForSemanticClassificationAsync(string expectedClassification, CancellationToken cancellationToken, int count = 1, bool exact = false)
3538
{
3639
var textView = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
3740
var classifier = await GetClassifierAsync(textView, cancellationToken);
@@ -42,7 +45,7 @@ public async Task WaitForSemanticClassificationAsync(string expectedClassificati
4245
classifier.ClassificationChanged += Classifier_ClassificationChanged;
4346

4447
// Check that we're not ALREADY changed
45-
if (HasClassification(classifier, textView, expectedClassification, count))
48+
if (HasClassification(classifier, textView, expectedClassification, count, exact))
4649
{
4750
semaphore.Release();
4851
classifier.ClassificationChanged -= Classifier_ClassificationChanged;
@@ -60,13 +63,13 @@ public async Task WaitForSemanticClassificationAsync(string expectedClassificati
6063

6164
void Classifier_ClassificationChanged(object sender, ClassificationChangedEventArgs e)
6265
{
63-
if (HasClassification(classifier, textView, expectedClassification, count))
66+
if (HasClassification(classifier, textView, expectedClassification, count, exact))
6467
{
6568
semaphore.Release();
6669
}
6770
}
6871

69-
static bool HasClassification(IClassifier classifier, ITextView textView, string expectedClassification, int count)
72+
static bool HasClassification(IClassifier classifier, ITextView textView, string expectedClassification, int count, bool exact)
7073
{
7174
var classifications = GetClassifications(classifier, textView);
7275

@@ -80,7 +83,8 @@ static bool HasClassification(IClassifier classifier, ITextView textView, string
8083
}
8184
}
8285

83-
return found >= count;
86+
return found == count ||
87+
(!exact && found > count);
8488
}
8589

8690
static bool ClassificationMatches(string expectedClassification, IClassificationType classificationType)

src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Commands.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public async Task InvokeRenameAsync(CancellationToken cancellationToken)
5858
var commandId = VSStd2KCmdID.RENAME;
5959

6060
// Rename seems to be extra-succeptable to COM exceptions
61-
await Helper.RetryAsync<bool?>(async (cancellationToken) => {
61+
await Helper.RetryAsync<bool?>(async (cancellationToken) =>
62+
{
6263
await ExecuteCommandAsync(commandGuid, (uint)commandId, cancellationToken);
6364
return true;
6465
}, TimeSpan.FromSeconds(1), cancellationToken);
@@ -69,15 +70,18 @@ public async Task CloseCodeFileAsync(string projectName, string relativeFilePath
6970
await CloseFileAsync(projectName, relativeFilePath, VSConstants.LOGVIEWID.Code_guid, saveFile, cancellationToken);
7071
}
7172

72-
public async Task CloseCurrentlyFocusedWindowAsync(CancellationToken cancellationToken)
73+
public async Task CloseCurrentlyFocusedWindowAsync(CancellationToken cancellationToken, bool save = false)
7374
{
7475
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
7576

7677
var monitorSelection = await GetRequiredGlobalServiceAsync<SVsShellMonitorSelection, IVsMonitorSelection>(cancellationToken);
7778
ErrorHandler.ThrowOnFailure(monitorSelection.GetCurrentElementValue((uint)VSSELELEMID.SEID_WindowFrame, out var windowFrameObj));
7879
var windowFrame = (IVsWindowFrame)windowFrameObj;
7980

80-
ErrorHandler.ThrowOnFailure(windowFrame.CloseFrame((uint)__FRAMECLOSE.FRAMECLOSE_NoSave));
81+
var closeFlags = save
82+
? __FRAMECLOSE.FRAMECLOSE_SaveIfDirty
83+
: __FRAMECLOSE.FRAMECLOSE_NoSave;
84+
ErrorHandler.ThrowOnFailure(windowFrame.CloseFrame((uint)closeFlags));
8185
}
8286

8387
private async Task ExecuteCommandAsync(Guid commandGuid, uint commandId, CancellationToken cancellationToken)

src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_LightBulb.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System.Collections.Generic;
5+
using System.Linq;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.VisualStudio.Language.Intellisense;
@@ -24,6 +25,15 @@ public async Task DismissLightBulbSessionAsync(CancellationToken cancellationTok
2425
broker.DismissSession(view);
2526
}
2627

28+
public async Task InvokeCodeActionAsync(string codeActionTitle, CancellationToken cancellationToken)
29+
{
30+
var codeActions = await ShowLightBulbAsync(cancellationToken);
31+
32+
var codeAction = codeActions.First(a => a.Actions.Single().DisplayText == codeActionTitle).Actions.Single();
33+
34+
await InvokeCodeActionAsync(codeAction, cancellationToken);
35+
}
36+
2737
public async Task<IEnumerable<SuggestedActionSet>> InvokeCodeActionListAsync(CancellationToken cancellationToken)
2838
{
2939
var lightbulbs = await ShowLightBulbAsync(cancellationToken);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Xunit;
6+
7+
namespace Microsoft.VisualStudio.Razor.IntegrationTests;
8+
9+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
10+
public class ManualRunOnlyIdeFactAttribute : IdeFactAttribute
11+
{
12+
public ManualRunOnlyIdeFactAttribute()
13+
{
14+
if (Environment.GetEnvironmentVariable("_IntegrationTestsRunningInCI") is not null)
15+
{
16+
Skip = "This test can only run manually";
17+
}
18+
}
19+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Razor.Logging;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
namespace Microsoft.VisualStudio.Razor.IntegrationTests;
13+
14+
[IdeSettings(MinVersion = VisualStudioVersion.VS2022, RootSuffix = "RoslynDev", MaxAttempts = 1)]
15+
public abstract class AbstractStressTest(ITestOutputHelper testOutputHelper) : AbstractRazorEditorTest(testOutputHelper)
16+
{
17+
protected override bool AllowDebugFails => true;
18+
19+
protected Task RunStressTestAsync(Func<int, CancellationToken, Task> iterationFunc)
20+
=> RunStressTestAsync(iterationFunc, TimeSpan.FromHours(1), TimeSpan.FromMinutes(1));
21+
22+
protected async Task RunStressTestAsync(Func<int, CancellationToken, Task> iterationFunc, TimeSpan maxRunTime, TimeSpan iterationTimeout)
23+
{
24+
var min = long.MaxValue;
25+
var max = long.MinValue;
26+
var avg = 0L;
27+
28+
var i = 0;
29+
var start = DateTime.Now;
30+
while (DateTime.Now.Subtract(maxRunTime) < start)
31+
{
32+
var iterationStart = Stopwatch.GetTimestamp();
33+
Logger.LogInformation($"**** Test iteration started: #{i} at {DateTime.Now}");
34+
35+
using var iterationCts = new CancellationTokenSource(iterationTimeout);
36+
var iterationToken = iterationCts.Token;
37+
38+
await iterationFunc(i, iterationToken);
39+
i++;
40+
41+
var duration = Stopwatch.GetTimestamp() - iterationStart;
42+
min = Math.Min(min, duration);
43+
max = Math.Max(max, duration);
44+
avg = ((avg * (i - 1)) + duration) / i;
45+
Logger.LogInformation($"**** Test iteration finished: #{i} in {TimeSpan.FromTicks(duration).TotalMilliseconds}ms");
46+
Logger.LogInformation($"**** Test iteration duration: min={TimeSpan.FromTicks(min).TotalMilliseconds}ms, max={TimeSpan.FromTicks(max).TotalMilliseconds}ms, avg={TimeSpan.FromTicks(avg).TotalMilliseconds}ms");
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)