Skip to content
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

Ensure build is shared across all test classes #12

Merged
merged 30 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
4 changes: 2 additions & 2 deletions visual-dotnet/SauceLabs.Visual.Tests/VisualApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace SauceLabs.Visual.Tests;

public class VisualApiTest
{
internal VisualApi<MockedWebDriver>? Api { get; set; }
internal VisualApi? Api { get; set; }
internal MockedWebDriver? WdMock { get; set; }
internal MockHttpMessageHandler? MockedHandler { get; set; }

Expand All @@ -27,7 +27,7 @@ public void Setup()

var caps = new MockedCapabilities(new Dictionary<string, object> { { "jobUuid", _jobUuid } });
WdMock = new MockedWebDriver(caps, _jobUuid);
Api = new VisualApi<MockedWebDriver>(WdMock, Region.Staging, _username, _accessKey, MockedHandler.ToHttpClient());
Api = new VisualApi(Region.Staging, _username, _accessKey, MockedHandler.ToHttpClient());
}

[Test]
Expand Down
172 changes: 172 additions & 0 deletions visual-dotnet/SauceLabs.Visual/BuildFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SauceLabs.Visual.GraphQL;
using SauceLabs.Visual.Utils;

namespace SauceLabs.Visual
{
internal static class BuildFactory
{
private static readonly Dictionary<string, VisualBuild> Builds = new Dictionary<string, VisualBuild>();

internal static async Task<VisualBuild> Get(VisualApi api, CreateBuildOptions options)
{
// Check if there is already a build for the current region.
if (Builds.TryGetValue(api.Region.Name, out var build))
{
return build;
}

var createdBuild = await Create(api, options);
Builds[api.Region.Name] = createdBuild;
return createdBuild;
}

/// <summary>
/// <c>Disregard</c> removes the build from the known builds.
/// </summary>
/// <param name="build">the build to remove</param>
private static void Disregard(VisualBuild build)
{
string? key = null;
var enumerator = Builds.GetEnumerator();
while (enumerator.MoveNext())
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
{
if (enumerator.Current.Value != build)
{
continue;
}
key = enumerator.Current.Key;
break;
}
enumerator.Dispose();

if (key != null)
{
Builds.Remove(key);
}
}

/// <summary>
/// <c>FindBuildById</c> returns the build identified by <c>buildId</c>
/// </summary>
/// <param name="api">a <c>VisualApi</c> object</param>
/// <param name="buildId">the <c>buildId</c> to look for</param>
/// <returns>the matching build</returns>
/// <exception cref="VisualClientException">when build is not existing or has an invalid state</exception>
private static async Task<VisualBuild> FindBuildById(VisualApi api, string buildId)
{
try
{
var build = (await api.Build(buildId)).EnsureValidResponse().Result;
return new VisualBuild(build.Id, build.Url, build.Mode);
}
catch (VisualClientException)
{
throw new VisualClientException($@"build {buildId} was not found");
}
}

/// <summary>
/// <c>FindBuildById</c> returns the build identified by <c>buildId</c>
/// </summary>
/// <param name="api">a <c>VisualApi</c> object</param>
/// <param name="customId">the <c>customId</c> to look for</param>
/// <returns>the matching build</returns>
/// <exception cref="VisualClientException">when build is not existing or has an invalid state</exception>
private static async Task<VisualBuild?> FindBuildByCustomId(VisualApi api, string customId)
{
try
{
var build = (await api.BuildByCustomId(customId)).EnsureValidResponse().Result;
return new VisualBuild(build.Id, build.Url, build.Mode);
}
catch (VisualClientException)
{
return null;
}
}

/// <summary>
/// <c>CloseBuilds</c> closes all build that are still open.
/// </summary>
internal static async Task CloseBuilds()
{
var builds = Builds.Values.ToArray();
foreach (var build in builds)
{
if (build.Close != null)
{
await build.Close();
}
}
}

/// <summary>
/// <c>CreateBuild</c> creates a new Visual build.
/// </summary>
/// <param name="client">the client used for the build creation</param>
/// <param name="options">the options for the build creation</param>
/// <returns>a <c>VisualBuild</c> instance</returns>
private static async Task<VisualBuild> Create(VisualApi api, CreateBuildOptions options)
{
var build = await GetEffectiveBuild(api, EnvVars.BuildId, EnvVars.CustomId);
if (build != null)
{
if (!build.IsRunning())
{
throw new VisualClientException($"build {build.Id} is not RUNNING");
}

build.IsExternal = true;
build.Close = async () => Disregard(build);

Check warning on line 123 in visual-dotnet/SauceLabs.Visual/BuildFactory.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 123 in visual-dotnet/SauceLabs.Visual/BuildFactory.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 123 in visual-dotnet/SauceLabs.Visual/BuildFactory.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 123 in visual-dotnet/SauceLabs.Visual/BuildFactory.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
return build;
}

options.CustomId ??= EnvVars.CustomId;
var result = (await api.CreateBuild(new CreateBuildIn
{
Name = options.Name,
Project = options.Project,
Branch = options.Branch,
CustomId = options.CustomId,
DefaultBranch = options.DefaultBranch,
})).EnsureValidResponse();

build = new VisualBuild(result.Result.Id, result.Result.Url, result.Result.Mode)
{
IsExternal = false
};

var copiedApi = api.Clone();
build.Close = async () =>
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
{
await copiedApi.FinishBuild(build.Id);
Disregard(build);
};
return build;
}

/// <summary>
/// <c>GetEffectiveBuild</c> tries to find the build matching the criterion provided by the user.
/// </summary>
/// <param name="api"></param>
/// <param name="buildId"></param>
/// <param name="customId"></param>
/// <returns></returns>
private static async Task<VisualBuild?> GetEffectiveBuild(VisualApi api, string? buildId, string? customId)
{
if (!StringUtils.IsNullOrEmpty(buildId))
{
return await FindBuildById(api, buildId!.Trim());
}

if (!StringUtils.IsNullOrEmpty(customId))
{
return await FindBuildByCustomId(api, customId!.Trim());
}
return null;
}
}
}
15 changes: 9 additions & 6 deletions visual-dotnet/SauceLabs.Visual/Region.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ namespace SauceLabs.Visual
/// </summary>
public class Region
{
private Region(Uri value)
private Region(string name, Uri value)
{
Value = value;
Name = name;
}

private Region(string value)
private Region(string name, string value)
{
Name = name;
Value = new Uri(value);
}

public Uri Value { get; }
public string Name { get; }

public override string ToString()
{
Expand All @@ -43,9 +46,9 @@ public static Region FromName(string name)
};
}

public static Region UsWest1 => new Region("https://api.us-west-1.saucelabs.com/v1/visual/graphql");
public static Region UsEast4 => new Region("https://api.us-east-4.saucelabs.com/v1/visual/graphql");
public static Region EuCentral1 => new Region("https://api.eu-central-1.saucelabs.com/v1/visual/graphql");
public static Region Staging => new Region("https://api.staging.saucelabs.net/v1/visual/graphql");
public static Region UsWest1 => new Region("us-west-1", "https://api.us-west-1.saucelabs.com/v1/visual/graphql");
public static Region UsEast4 => new Region("us-east-4", "https://api.us-east-4.saucelabs.com/v1/visual/graphql");
public static Region EuCentral1 => new Region("eu-central-1", "https://api.eu-central-1.saucelabs.com/v1/visual/graphql");
public static Region Staging => new Region("staging", "https://api.staging.saucelabs.net/v1/visual/graphql");
}
}
13 changes: 10 additions & 3 deletions visual-dotnet/SauceLabs.Visual/VisualApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
using Newtonsoft.Json;
using OpenQA.Selenium;
using SauceLabs.Visual.GraphQL;
using SauceLabs.Visual.Utils;

namespace SauceLabs.Visual
{
internal class VisualApi<T> : IDisposable where T : IHasCapabilities, IHasSessionId
internal class VisualApi : IDisposable
{
internal readonly Region Region;
private readonly string _username;
private readonly string _accessKey;
private readonly GraphQLHttpClient _graphQlClient;

public VisualApi(T webdriver, Region region, string username, string accessKey, HttpClient? httpClient = null)
public VisualApi(Region region, string username, string accessKey, HttpClient? httpClient = null)
{

if (StringUtils.IsNullOrEmpty(username) || StringUtils.IsNullOrEmpty(accessKey))
{
throw new VisualClientException(
"Invalid SauceLabs credentials. Please check your SauceLabs username and access key at https://app.saucelabs.com/user-setting");
}

Region = region;
_username = username.Trim();
_accessKey = accessKey.Trim();

Expand Down Expand Up @@ -117,5 +119,10 @@ public void Dispose()
{
_graphQlClient.Dispose();
}

internal VisualApi Clone()
{
return new VisualApi(Region, _username, _accessKey);
}
}
}
5 changes: 5 additions & 0 deletions visual-dotnet/SauceLabs.Visual/VisualBuild.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Threading.Tasks;
using SauceLabs.Visual.Models;

namespace SauceLabs.Visual
Expand All @@ -12,6 +14,9 @@ public class VisualBuild

public BuildMode Mode { get; internal set; }

internal bool IsExternal { get; set; } = false;
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
internal Func<Task>? Close;

internal VisualBuild(string id, string url, BuildMode mode)
{
Id = id;
Expand Down
2 changes: 1 addition & 1 deletion visual-dotnet/SauceLabs.Visual/VisualCheckOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class VisualCheckOptions

internal void EnsureTestContextIsPopulated(string callerMemberName, string? previousSuiteName)
{
if (string.IsNullOrEmpty(callerMemberName) || HasIncompleteTestContext())
if (string.IsNullOrEmpty(callerMemberName) || !HasIncompleteTestContext())
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}
Expand Down
Loading