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

Preserve logs from retries as they have data! #19341

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ public override ApplicationState AppState

return Convert.ToInt32(state) switch
{
0 => ApplicationState.Not_Installed,
1 => ApplicationState.Not_Running,
0 => ApplicationState.NotInstalled,
1 => ApplicationState.NotRunning,
3 or
4 => ApplicationState.Running,
_ => ApplicationState.Unknown,
Expand Down
30 changes: 5 additions & 25 deletions src/TestUtils/src/UITest.Appium/AppiumApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@

namespace UITest.Appium
{
public abstract class AppiumApp : IApp
public abstract class AppiumApp : IApp, IScreenshotSupportedApp
{
protected readonly AppiumDriver _driver;
protected readonly IConfig _config;
protected readonly AppiumCommandExecutor _commandExecutor;

public AppiumApp(AppiumDriver driver, IConfig config)
{
_driver = driver;
_config = config;
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
_config = config ?? throw new ArgumentNullException(nameof(config));

_commandExecutor = new AppiumCommandExecutor();
_commandExecutor.AddCommandGroup(new AppiumPointerActions(this));
Expand All @@ -33,36 +33,16 @@ public AppiumApp(AppiumDriver driver, IConfig config)
public ICommandExecution CommandExecutor => _commandExecutor;
public string ElementTree => _driver.PageSource;

public void Click(float x, float y)
{
CommandExecutor.Execute("click", new Dictionary<string, object>()
{
{ "x", x },
{ "y", y }
});
}

public FileInfo Screenshot(string fileName)
{
if (_driver == null)
{
throw new NullReferenceException("Screenshot: _driver is null");
}

string filename = $"{fileName}.png";
Screenshot screenshot = _driver.GetScreenshot();
Comment on lines -52 to 38
Copy link
Member Author

Choose a reason for hiding this comment

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

Don't force the .png here (even though it is a png) since this makes the other part of the code hard to reuse.

screenshot.SaveAsFile(filename, ScreenshotImageFormat.Png);
var file = new FileInfo(filename);
screenshot.SaveAsFile(fileName, ScreenshotImageFormat.Png);
var file = new FileInfo(fileName);
return file;
}

public byte[] Screenshot()
{
if (_driver == null)
{
throw new NullReferenceException("Screenshot: _driver is null");
}

Screenshot screenshot = _driver.GetScreenshot();
return screenshot.AsByteArray;
}
Expand Down
2 changes: 1 addition & 1 deletion src/TestUtils/src/UITest.Appium/AppiumCatalystApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public override ApplicationState AppState
// https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc
return Convert.ToInt32(state) switch
{
1 => ApplicationState.Not_Running,
1 => ApplicationState.NotRunning,
2 or
3 or
4 => ApplicationState.Running,
Expand Down
2 changes: 1 addition & 1 deletion src/TestUtils/src/UITest.Appium/AppiumIOSApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public override ApplicationState AppState
// https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc
return Convert.ToInt32(state) switch
{
1 => ApplicationState.Not_Running,
1 => ApplicationState.NotRunning,
2 or
3 or
4 => ApplicationState.Running,
Expand Down
2 changes: 1 addition & 1 deletion src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public override ApplicationState AppState
}
catch (NoSuchWindowException)
{
return ApplicationState.Not_Running;
return ApplicationState.NotRunning;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/TestUtils/src/UITest.Core/ApplicationState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
{
public enum ApplicationState
{
Not_Installed,
NotInstalled,
Installed,
Not_Running,
NotRunning,
jfversluis marked this conversation as resolved.
Show resolved Hide resolved
Running,
Unknown
}
Expand Down
40 changes: 38 additions & 2 deletions src/TestUtils/src/UITest.Core/IApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,50 @@ public interface IApp : IDisposable
IConfig Config { get; }
IUIElementQueryable Query { get; }
ApplicationState AppState { get; }

IUIElement FindElement(string id);
IUIElement FindElement(IQuery query);
IReadOnlyCollection<IUIElement> FindElements(string id);
IReadOnlyCollection<IUIElement> FindElements(IQuery query);
string ElementTree { get; }

ICommandExecution CommandExecutor { get; }
void Click(float x, float y);
}

public interface IScreenshotSupportedApp : IApp
{
FileInfo Screenshot(string fileName);
byte[] Screenshot();
string ElementTree { get; }
}
Comment on lines +18 to +22
Copy link
Member Author

Choose a reason for hiding this comment

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

"optional APIs" should not be forced on the core testing IApp. Things like device logs and screenshots are only available on some platforms and in somace situations.


public static class AppExtensions
{
public static void Click(this IApp app, float x, float y)
{
app.CommandExecutor.Execute("click", new Dictionary<string, object>()
{
{ "x", x },
{ "y", y }
});
}


internal static T As<T>(this IApp app)
where T : IApp
{
if (app is not T derivedApp)
throw new NotImplementedException($"The app '{app}' does not implement '{typeof(T).FullName}'.");

return derivedApp;
}
}

public static class ScreenshotSupportedAppExtensions
{
public static FileInfo Screenshot(this IApp app, string fileName) =>
app.As<IScreenshotSupportedApp>().Screenshot(fileName);

public static byte[] Screenshot(this IApp app) =>
app.As<IScreenshotSupportedApp>().Screenshot();
}
}
73 changes: 42 additions & 31 deletions src/TestUtils/src/UITest.NUnit/UITestBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using UITest.Core;
Expand Down Expand Up @@ -55,21 +56,21 @@ protected virtual void FixtureTeardown()
[TearDown]
public void UITestBaseTearDown()
{

if (App.AppState == ApplicationState.Not_Running)
if (App.AppState == ApplicationState.NotRunning)
{
// Assert.Fail will immediately exit the test which is desirable as the app is not
// running anymore so we don't want to log diagnostic data as there is nothing to collect from
Reset();
FixtureSetup();

// Assert.Fail will immediately exit the test which is desirable as the app is not
// running anymore so we can't capture any UI structures or any screenshots
Assert.Fail("The app was expected to be running still, investigate as possible crash");
}

var testOutcome = TestContext.CurrentContext.Result.Outcome;
if (testOutcome == ResultState.Error ||
testOutcome == ResultState.Failure)
{
SaveDiagnosticLogs("UITestBaseTearDown");
SaveUIDiagnosticInfo();
}
}

Expand All @@ -84,7 +85,7 @@ public void OneTimeSetup()
}
catch
{
SaveDiagnosticLogs("FixtureSetup");
SaveUIDiagnosticInfo();
throw;
}
}
Expand All @@ -98,36 +99,53 @@ public void OneTimeTearDown()
if (outcome.Status == ResultState.SetUpFailure.Status &&
outcome.Site == ResultState.SetUpFailure.Site)
{
SaveDiagnosticLogs("OneTimeTearDown");
SaveUIDiagnosticInfo();
}

FixtureTeardown();
}

void SaveDiagnosticLogs(string? note = null)
void SaveUIDiagnosticInfo([CallerMemberName] string? note = null)
{
var screenshotPath = GetGeneratedFilePath("ScreenShot.png", note);
if (screenshotPath is not null)
{
_ = App.Screenshot(screenshotPath);

AddTestAttachment(screenshotPath, Path.GetFileName(screenshotPath));
}

var pageSourcePath = GetGeneratedFilePath("PageSource.txt", note);
if (pageSourcePath is not null)
{
File.WriteAllText(pageSourcePath, App.ElementTree);

AddTestAttachment(pageSourcePath, Path.GetFileName(pageSourcePath));
}
}

string? GetGeneratedFilePath(string filename, string? note = null)
{
Comment on lines +127 to +128
Copy link
Member Author

Choose a reason for hiding this comment

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

This new method makes sure to get a new filename per run so that all logs are preserved.

Copy link
Member

Choose a reason for hiding this comment

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

But can we have a way to pass the guid from the cake? so we can map to what is on ci ui display name?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure, there might be multiple guids per run. If the test run crashes or appium somehow needs a restart, then we may have 2 items per single cake run.

// App could be null if UITestContext was not able to connect to the test process (e.g. port already in use etc...)
if (UITestContext is null)
return null;

if (string.IsNullOrEmpty(note))
note = "-";
else
note = $"-{note}-";

var logDir = (Path.GetDirectoryName(Environment.GetEnvironmentVariable("APPIUM_LOG_FILE")) ?? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))!;
filename = $"{Path.GetFileNameWithoutExtension(filename)}-{Guid.NewGuid().ToString("N")}{Path.GetExtension(filename)}";

// App could be null if UITestContext was not able to connect to the test process (e.g. port already in use etc...)
if (UITestContext is not null)
{
string name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
var logDir =
Path.GetDirectoryName(Environment.GetEnvironmentVariable("APPIUM_LOG_FILE") ??
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))!;

var screenshotPath = Path.Combine(logDir, $"{name}-{_testDevice}{note}ScreenShot");
_ = App.Screenshot(screenshotPath);
// App.Screenshot appends a ".png" extension always, so include that here
var screenshotPathWithExtension = screenshotPath + ".png";
AddTestAttachment(screenshotPathWithExtension, Path.GetFileName(screenshotPathWithExtension));
var name =
TestContext.CurrentContext.Test.MethodName ??
TestContext.CurrentContext.Test.Name;

var pageSourcePath = Path.Combine(logDir, $"{name}-{_testDevice}{note}PageSource.txt");
File.WriteAllText(pageSourcePath, App.ElementTree);
AddTestAttachment(pageSourcePath, Path.GetFileName(pageSourcePath));
}
return Path.Combine(logDir, $"{name}-{_testDevice}{note}{filename}");
}

void AddTestAttachment(string filePath, string? description = null)
Expand All @@ -136,17 +154,10 @@ void AddTestAttachment(string filePath, string? description = null)
{
TestContext.AddTestAttachment(filePath, description);
}
catch (FileNotFoundException e)
catch (FileNotFoundException e) when (e.Message == "Test attachment file path could not be found.")
{
// Add the file path to better troubleshoot when these errors occur
if (e.Message == "Test attachment file path could not be found.")
{
throw new FileNotFoundException($"{e.Message}: {filePath}");
}
else
{
throw;
}
throw new FileNotFoundException($"Test attachment file path could not be found: '{filePath}' {description}", e);
}
}
}
Expand Down