Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
## [1.4.1] - 2023-11-27
- Multiple improvements to the UI, including better dropdowns, filtering, and a new test list view for Player.
- Fixed uncategorized UI tests filtering for parameterized tests (DSTR-219).
- In async tests, any failing logs will now first be evaluated after the async method has completed. (DSTR-839)
  • Loading branch information
Unity Technologies committed Nov 27, 2023
1 parent ac12a65 commit d7833c1
Show file tree
Hide file tree
Showing 66 changed files with 1,878 additions and 1,131 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Changelog
## [1.4.1] - 2023-11-27
- Multiple improvements to the UI, including better dropdowns, filtering, and a new test list view for Player.
- Fixed uncategorized UI tests filtering for parameterized tests (DSTR-219).
- In async tests, any failing logs will now first be evaluated after the async method has completed. (DSTR-839)

## [1.4.0] - 2023-11-10
- Added api for saving test results to a file.
- Added a button for saving the test results of the latest run.
Expand Down
2 changes: 2 additions & 0 deletions Documentation~/reference-async-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

You can use the dotnet Task asynchronous programming model to write asynchronous tests. If you're new to asynchronous programming and its applications, see the [Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) for a comprehensive guide. See also the documentation for NUnit [Test](https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html), which explains the requirements for an async test. Async code is run on the main thread and Unity Test Framework will `await` it by checking if the task is done on each [update](https://docs.unity3d.com/ScriptReference/PlayerLoop.Update.html) for Play Mode or on each [EditorApplication.update](https://docs.unity3d.com/ScriptReference/EditorApplication-update.html) outside Play Mode.

Note that any failing log messages will first be evaluated after the async test has completed. This means that if you have a failing log message in an async test, it will not be reported until the test has completed.

The following code snippet demonstrates an async test based on Microsoft's making breakfast example. Note that the test method is marked with the `async` keyword and has return type `Task`. We set up a list of Tasks corresponding to asynchronous methods representing parts of the breakfast making process. We use `await` to start these tasks in a non-blocking way, write to the log when each one completes, and write again to the log when all are completed.

```
Expand Down
8 changes: 8 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/BitUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Provides methods for dealing with common bit operations.
/// </summary>
internal static class BitUtility
{
/// <summary>
/// Evaluates the cardinality of an integer, treating the value as a bit set.
/// Optimization based on http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel.
/// </summary>
/// <param name="integer">The input integer value.</param>
/// <returns>The number of bits set in the provided input integer value.</returns>
internal static int GetCardinality(int integer)
{
unchecked
{
integer = integer - ((integer >> 1) & 0x55555555);
integer = (integer & 0x33333333) + ((integer >> 2) & 0x33333333);
integer = (((integer + (integer >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24;
}

return integer;
}
}
}
3 changes: 3 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/BitUtility.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/FlagEnumContentProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using UnityEngine;

namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// A flag enum content provider to be used with the <see cref="SelectionDropDown" /> control.
/// </summary>
/// <typeparam name="T">The flag enum type.</typeparam>
internal class FlagEnumContentProvider<T> : ISelectionDropDownContentProvider where T : Enum
{
private readonly Action<T> m_ValueChangedCallback;
private readonly T[] m_Values;
internal Func<string, string> DisplayNameGenerator = ObjectNames.NicifyVariableName;
private T m_CurrentValue;

/// <summary>
/// Creates a new instance of the <see cref="FlagEnumContentProvider{T}" /> class.
/// </summary>
/// <param name="initialValue">The initial selection value.</param>
/// <param name="valueChangedCallback">The callback to be invoked on selection change.</param>
/// <exception cref="ArgumentException">
/// Thrown if the generic enum parameter type is not integer based
/// or if the initial selection value is empty.
/// </exception>
/// <exception cref="ArgumentNullException">Thrown if the provided change callback is null.</exception>
public FlagEnumContentProvider(T initialValue, Action<T> valueChangedCallback)
{
if (Enum.GetUnderlyingType(typeof(T)) != typeof(int))
{
throw new ArgumentException("Argument underlying type must be integer.");
}

if ((int)(object)initialValue == 0)
{
throw new ArgumentException("The initial value must not be an empty set.", nameof(initialValue));
}

if (valueChangedCallback == null)
{
throw new ArgumentNullException(nameof(valueChangedCallback), "The value change callback must not be null.");
}

m_CurrentValue = initialValue;
m_Values = (T[])Enum.GetValues(typeof(T));
m_ValueChangedCallback = valueChangedCallback;
}

public int Count => m_Values.Length;
public bool IsMultiSelection => true;

public string GetName(int index)
{
return ValidateIndexBounds(index) ? DisplayNameGenerator(m_Values[index].ToString()) : string.Empty;
}

public int[] SeparatorIndices => new int[0];

public bool IsSelected(int index)
{
return ValidateIndexBounds(index) && IsSet(m_Values[index]);
}

public void SelectItem(int index)
{
if (!ValidateIndexBounds(index))
{
return;
}

if (ChangeValue(m_Values[index]))
{
m_ValueChangedCallback(m_CurrentValue);
}
}

private bool ChangeValue(T flag)
{
var value = flag;
var count = GetSetCount();

if (IsSet(value))
{
if (count == 1)
{
return false;
}

m_CurrentValue = FlagEnumUtility.RemoveFlag(m_CurrentValue, flag);
return true;
}

m_CurrentValue = FlagEnumUtility.SetFlag(m_CurrentValue, flag);
return true;
}

private bool IsSet(T flag)
{
return FlagEnumUtility.HasFlag(m_CurrentValue, flag);
}

private int GetSetCount()
{
return BitUtility.GetCardinality((int)(object)m_CurrentValue);
}

private bool ValidateIndexBounds(int index)
{
if (index < 0 || index >= Count)
{
Debug.LogError($"Requesting item index {index} from a collection of size {Count}");
return false;
}

return true;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/FlagEnumUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using UnityEngine;

namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Provides methods for dealing with common enumerator operations.
/// </summary>
internal static class FlagEnumUtility
{
/// <summary>
/// Checks for the presence of a flag in a flag enum value.
/// </summary>
/// <param name="value">The value to check for the presence of the flag.</param>
/// <param name="flag">The flag whose presence is to be checked.</param>
/// <typeparam name="T">The flag enum type.</typeparam>
/// <returns></returns>
internal static bool HasFlag<T>(T value, T flag) where T : Enum
{
ValidateUnderlyingType<T>();

var intValue = (int)(object)value;
var intFlag = (int)(object)flag;
return (intValue & intFlag) == intFlag;
}

/// <summary>
/// Sets a flag in a flag enum value.
/// </summary>
/// <param name="value">The value where the flag should be set.</param>
/// <param name="flag">The flag to be set.</param>
/// <typeparam name="T">The flag enum type.</typeparam>
/// <returns>The input value with the flag set.</returns>
internal static T SetFlag<T>(T value, T flag) where T : Enum
{
ValidateUnderlyingType<T>();

var intValue = (int)(object)value;
var intFlag = (int)(object)flag;
var result = intValue | intFlag;
return (T)Enum.ToObject(typeof(T), result);
}

/// <summary>
/// Removes a flag in a flag enum value.
/// </summary>
/// <param name="value">The value where the flag should be removed.</param>
/// <param name="flag">The flag to be removed.</param>
/// <typeparam name="T">The flag enum type.</typeparam>
/// <returns>The input value with the flag removed.</returns>
internal static T RemoveFlag<T>(T value, T flag) where T : Enum
{
ValidateUnderlyingType<T>();

var intValue = (int)(object)value;
var intFlag = (int)(object)flag;
var result = intValue & ~intFlag;
return (T)Enum.ToObject(typeof(T), result);
}

/// <summary>
/// Validates that the underlying type of an enum is integer.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <exception cref="ArgumentException">Thrown if the underlying type of the enum type parameter is not integer.</exception>
private static void ValidateUnderlyingType<T>() where T : Enum
{
if (Enum.GetUnderlyingType(typeof(T)) != typeof(int))
{
throw new ArgumentException("Argument underlying type must be integer.");
}
}
}
}
3 changes: 3 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/FlagEnumUtility.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 101 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/GenericItemContentProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.Linq;
using UnityEngine;

namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// A generic type content provider to be used with the <see cref="SelectionDropDown" /> control.
/// </summary>
/// <typeparam name="T">The type of values represented by content elements.</typeparam>
internal class GenericItemContentProvider<T> : ISelectionDropDownContentProvider where T : IEquatable<T>
{
private readonly ISelectableItem<T>[] m_Items;
private readonly Action<T> m_ValueChangedCallback;
private T m_CurrentValue;

/// <summary>
/// Creates a new instance of the <see cref="GenericItemContentProvider{T}" /> class.
/// </summary>
/// <param name="initialValue">The initial selection value.</param>
/// <param name="items">The set of selectable items.</param>
/// <param name="separatorIndices">The indices of items which should be followed by separator lines.</param>
/// <param name="valueChangedCallback"></param>
/// <exception cref="ArgumentNullException">Thrown if any of the provided arguments is null, except for the separator indices.</exception>
/// <exception cref="ArgumentException">Thrown if the set of items is empty or does not contain the initial selection value.</exception>
public GenericItemContentProvider(T initialValue, ISelectableItem<T>[] items, int[] separatorIndices, Action<T> valueChangedCallback)
{
if (initialValue == null)
{
throw new ArgumentNullException(nameof(initialValue), "The initial selection value must not be null.");
}

if (items == null)
{
throw new ArgumentNullException(nameof(items), "The set of items must not be null.");
}

if (valueChangedCallback == null)
{
throw new ArgumentNullException(nameof(valueChangedCallback), "The value change callback must not be null.");
}

if (items.Length == 0)
{
throw new ArgumentException("The set of items must not be empty.", nameof(items));
}

if (!items.Any(i => i.Value.Equals(initialValue)))
{
throw new ArgumentException("The initial selection value must be in the items set.", nameof(items));
}

m_CurrentValue = initialValue;
m_Items = items;
SeparatorIndices = separatorIndices ?? new int[0];
m_ValueChangedCallback = valueChangedCallback;
}

public int Count => m_Items.Length;
public bool IsMultiSelection => false;

public string GetName(int index)
{
return ValidateIndexBounds(index) ? m_Items[index].DisplayName : string.Empty;
}

public int[] SeparatorIndices { get; }

public void SelectItem(int index)
{
if (!ValidateIndexBounds(index))
{
return;
}

if (IsSelected(index))
{
return;
}

m_CurrentValue = m_Items[index].Value;
m_ValueChangedCallback(m_CurrentValue);
}

public bool IsSelected(int index)
{
return ValidateIndexBounds(index) && m_Items[index].Value.Equals(m_CurrentValue);
}

private bool ValidateIndexBounds(int index)
{
if (index < 0 || index >= Count)
{
Debug.LogError($"Requesting item index {index} from a collection of size {Count}");
return false;
}

return true;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/ISelectableItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Defines a content element which can be used with the <see cref="GenericItemContentProvider{T}" /> content provider.
/// </summary>
internal interface ISelectableItem<out T>
{
/// <summary>
/// The value represented by this item.
/// </summary>
T Value { get; }

/// <summary>
/// The name to be used when displaying this item.
/// </summary>
string DisplayName { get; }
}
}
3 changes: 3 additions & 0 deletions UnityEditor.TestRunner/GUI/Controls/ISelectableItem.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d7833c1

Please sign in to comment.