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

Add BuildSceneAttribute and SceneManagerHelper.LoadSceneAsync method #73

Merged
merged 6 commits into from
Apr 29, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions Editor/TemporaryBuildScenesUsingInTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 Koji Hasegawa.
// Copyright (c) 2023-2024 Koji Hasegawa.
// This software is released under the MIT License.

using System;
Expand All @@ -7,7 +7,7 @@
using System.Linq;
using TestHelper.Attributes;
using TestHelper.Editor;
using TestHelper.Utils;
using TestHelper.RuntimeInternals;
using UnityEditor;
using UnityEditor.TestTools;
using UnityEngine;
Expand All @@ -21,50 +21,50 @@ namespace TestHelper.Editor
/// </summary>
public class TemporaryBuildScenesUsingInTest : ITestPlayerBuildModifier
{
private static IEnumerable<LoadSceneAttribute> FindLoadSceneAttributesOnAssemblies()
private static IEnumerable<T> FindAttributesOnAssemblies<T>() where T : Attribute
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var attribute in assemblies
.Select(assembly => assembly.GetCustomAttributes(typeof(LoadSceneAttribute), false))
.Select(assembly => assembly.GetCustomAttributes(typeof(T), false))
.SelectMany(attributes => attributes))
{
yield return attribute as LoadSceneAttribute;
yield return attribute as T;
}
}

private static IEnumerable<LoadSceneAttribute> FindLoadSceneAttributesOnTypes()
private static IEnumerable<T> FindAttributesOnTypes<T>() where T : Attribute
{
var symbols = TypeCache.GetTypesWithAttribute<LoadSceneAttribute>();
var symbols = TypeCache.GetTypesWithAttribute<T>();
foreach (var attribute in symbols
.Select(symbol => symbol.GetCustomAttributes(typeof(LoadSceneAttribute), false))
.Select(symbol => symbol.GetCustomAttributes(typeof(T), false))
.SelectMany(attributes => attributes))
{
yield return attribute as LoadSceneAttribute;
yield return attribute as T;
}
}

private static IEnumerable<LoadSceneAttribute> FindLoadSceneAttributesOnMethods()
private static IEnumerable<T> FindAttributesOnMethods<T>() where T : Attribute
{
var symbols = TypeCache.GetMethodsWithAttribute<LoadSceneAttribute>();
var symbols = TypeCache.GetMethodsWithAttribute<T>();
foreach (var attribute in symbols
.Select(symbol => symbol.GetCustomAttributes(typeof(LoadSceneAttribute), false))
.Select(symbol => symbol.GetCustomAttributes(typeof(T), false))
.SelectMany(attributes => attributes))
{
yield return attribute as LoadSceneAttribute;
yield return attribute as T;
}
}

internal static IEnumerable<string> GetScenesUsingInTest()
{
var attributes = FindLoadSceneAttributesOnAssemblies()
.Concat(FindLoadSceneAttributesOnTypes())
.Concat(FindLoadSceneAttributesOnMethods());
var attributes = FindAttributesOnAssemblies<BuildSceneAttribute>()
.Concat(FindAttributesOnTypes<BuildSceneAttribute>())
.Concat(FindAttributesOnMethods<BuildSceneAttribute>());
foreach (var attribute in attributes)
{
string scenePath;
try
{
scenePath = ScenePathFinder.GetExistScenePath(attribute.ScenePath);
scenePath = SceneManagerHelper.GetExistScenePath(attribute.ScenePath, attribute.CallerFilePath);
}
catch (ArgumentException e)
{
Expand Down
3 changes: 2 additions & 1 deletion Editor/TestHelper.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "TestHelper.Editor",
"rootNamespace": "TestHelper",
"references": [
"TestHelper"
"TestHelper",
"TestHelper.RuntimeInternals"
],
"includePlatforms": [
"Editor"
Expand Down
90 changes: 85 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ It has the following benefits:

- Can be use same code for running Edit Mode tests, Play Mode tests in Editor, and on Player.
- Can be specified scenes that are **NOT** in "Scenes in Build".
- Can be specified path by glob pattern. However, there are restrictions, top level and scene name cannot be omitted.
- Can be specified path by relative path from the test class file.
- Can be specified scene path by [glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern. However, there are restrictions, top level and scene name cannot be omitted.
- Can be specified scene path by relative path from the test class file.

- This attribute can attached to the test method only.
This attribute can attached to the test method only.
It can be used with sync Tests, async Tests, and UnityTest.

Usage:
Expand Down Expand Up @@ -258,8 +258,50 @@ public class MyTestClass
```

> [!NOTE]
> - Load scene run after `OneTimeSetUp` and before `SetUp`
> - Scene file path is starts with `Assets/` or `Packages/`. And package name using `name` instead of `displayName`, when scenes in the package. (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)
> - Load scene run after `OneTimeSetUp` and before `SetUp`. If you want to setup before loading Use [BuildSceneAttribute](#BuildScene) and [SceneManagerHelper](#SceneManagerHelper) method instead.
> - Scene file path is starts with `Assets/` or `Packages/` or `.`. And package name using `name` instead of `displayName`, when scenes in the package. (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)


#### BuildScene

`BuildSceneAttribute` is a NUnit test attribute class that build the scene before running the test on player.

It has the following benefits:

- Can be specified scenes that are **NOT** in "Scenes in Build".
- Can be specified scene path by [glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern. However, there are restrictions, top level and scene name cannot be omitted.
- Can be specified scene path by relative path from the test class file.

This attribute can attached to the test method only.
It can be used with sync Tests, async Tests, and UnityTest.

Usage:

```csharp
using NUnit.Framework;
using TestHelper.Attributes;
using TestHelper.RuntimeInternals;
using UnityEngine;

[TestFixture]
public class MyTestClass
{
[Test]
[BuildScene("../../Scenes/SampleScene.unity")]
public void MyTestMethod()
{
// Setup before load scene

// Load scene
await SceneManagerHelper.LoadSceneAsync("../../Scenes/SampleScene.unity");

// Excercise the test
}
}
```

> [!NOTE]
> - Scene file path is starts with `Assets/` or `Packages/` or `.`. And package name using `name` instead of `displayName`, when scenes in the package. (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)


#### TakeScreenshot
Expand Down Expand Up @@ -435,6 +477,44 @@ public class MyTestClass
> - UniTask is required to be used from the async method. And also needs coroutineRunner (any MonoBehaviour) because TakeScreenshot method uses WaitForEndOfFrame inside. See more information: https://github.com/Cysharp/UniTask#ienumeratortounitask-limitation


#### SceneManagerHelper

`SceneManagerHelper` is a utility class to load the scene file.

It has the following benefits:

- Can be use same code for running Edit Mode tests, Play Mode tests in Editor, and on Player.
- Can be specified scene path by [glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern. However, there are restrictions, top level and scene name cannot be omitted.
- Can be specified scene path by relative path from the test class file.

Usage:

```csharp
using NUnit.Framework;
using TestHelper.RuntimeInternals;
using UnityEngine;

[TestFixture]
public class MyTestClass
{
[Test]
public void MyTestMethod()
{
// Setup before load scene

// Load scene
await SceneManagerHelper.LoadSceneAsync("../../Scenes/SampleScene.unity");

// Excercise the test
}
}
```

> [!NOTE]
> - Scene file path is starts with `Assets/` or `Packages/` or `.`. And package name using `name` instead of `displayName`, when scenes in the package. (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)
> - When loading the scene that is not in "Scenes in Build", use [BuildSceneAttribute](#BuildScene).


### Editor Extensions

#### Open Persistent Data Directory
Expand Down
42 changes: 42 additions & 0 deletions Runtime/Attributes/BuildSceneAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2023-2024 Koji Hasegawa.
// This software is released under the MIT License.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using NUnit.Framework;

namespace TestHelper.Attributes
{
/// <summary>
/// Build scene before running test on player.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class BuildSceneAttribute : NUnitAttribute
{
internal string ScenePath { get; private set; }
internal string CallerFilePath { get; private set; }

/// <summary>
/// Build scene before running test on player.
/// This attribute has the following benefits:
/// - Can be specified scenes that are **NOT** in "Scenes in Build".
/// - Can be specified scene path by [glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern. However, there are restrictions, top level and scene name cannot be omitted.
/// - Can be specified scene path by relative path from the test class file.
/// </summary>
/// <param name="path">Scene file path.
/// The path starts with `Assets/` or `Packages/` or `.`.
/// And package name using `name` instead of `displayName`, when scenes in the package.
/// (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)
/// </param>
/// <remarks>
/// - For the process of including a Scene not in "Scenes in Build" to a build for player, see: <see cref="TestHelper.Editor.TemporaryBuildScenesUsingInTest"/>.
/// </remarks>
[SuppressMessage("ReSharper", "InvalidXmlDocComment")]
public BuildSceneAttribute(string path, [CallerFilePath] string callerFilePath = null)
{
ScenePath = path;
CallerFilePath = callerFilePath;
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Attributes/BuildSceneAttribute.cs.meta

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

100 changes: 17 additions & 83 deletions Runtime/Attributes/LoadSceneAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,114 +4,48 @@
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using TestHelper.Utils;
using UnityEngine;
using UnityEngine.SceneManagement;
using TestHelper.RuntimeInternals;
using UnityEngine.TestTools;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif

// ReSharper disable InvalidXmlDocComment

namespace TestHelper.Attributes
{
/// <summary>
/// Load scene before running test.
///
/// It has the following benefits:
/// - Can be used when running play mode tests in-editor and on-player
/// - Can be specified scenes that are not in "Scenes in Build"
///
/// Notes:
/// - Load scene run after <c>OneTimeSetUp</c> and before <c>SetUp</c>
/// - For the process of including a Scene not in "Scenes in Build" to a build for player, see: <see cref="Editor.TemporaryBuildScenesUsingInTest"/>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class LoadSceneAttribute : NUnitAttribute, IOuterUnityTestAction
public class LoadSceneAttribute : BuildSceneAttribute, IOuterUnityTestAction
{
internal string ScenePath { get; private set; }

/// <summary>
/// Load scene before running test.
/// Can be specified path by glob pattern. However, there are restrictions, top level and scene name cannot be omitted.
/// Can be specified relative path.
/// This attribute has the following benefits:
/// - Can be use same code for running Edit Mode tests, Play Mode tests in Editor, and on Player.
/// - Can be specified scenes that are **NOT** in "Scenes in Build".
/// - Can be specified scene path by [glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern. However, there are restrictions, top level and scene name cannot be omitted.
/// - Can be specified scene path by relative path from the test class file.
/// </summary>
/// <param name="path">Scene file path.
/// The path starts with `Assets/` or `Packages/`.
/// The path starts with `Assets/` or `Packages/` or `.`.
/// And package name using `name` instead of `displayName`, when scenes in the package.
/// (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)
/// </param>
/// <seealso href="https://en.wikipedia.org/wiki/Glob_(programming)"/>
/// <remarks>
/// - Load scene run after <c>OneTimeSetUp</c> and before <c>SetUp</c>. If you want to setup before loading Use <see cref="BuildSceneAttribute"/> and <see cref="SceneManagerHelper.LoadSceneAsync"/> instead.
/// - For the process of including a Scene not in "Scenes in Build" to a build for player, see: <see cref="TestHelper.Editor.TemporaryBuildScenesUsingInTest"/>.
/// </remarks>
[SuppressMessage("ReSharper", "ExplicitCallerInfoArgument")]
[SuppressMessage("ReSharper", "InvalidXmlDocComment")]
public LoadSceneAttribute(string path, [CallerFilePath] string callerFilePath = null)
: base(path, callerFilePath)
{
if (path.StartsWith("."))
{
ScenePath = GetAbsolutePath(path, callerFilePath);
}
else
{
ScenePath = path;
}
}

[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")]
internal static string GetAbsolutePath(string relativePath, string callerFilePath)
{
var callerDirectory = Path.GetDirectoryName(callerFilePath);
var absolutePath = Path.GetFullPath(Path.Combine(callerDirectory, relativePath));

var assetsIndexOf = absolutePath.IndexOf("Assets", StringComparison.Ordinal);
if (assetsIndexOf > 0)
{
return absolutePath.Substring(assetsIndexOf);
}

var packageIndexOf = absolutePath.IndexOf("Packages", StringComparison.Ordinal);
if (packageIndexOf > 0)
{
return absolutePath.Substring(packageIndexOf);
}

throw new ArgumentException(
$"Can not resolve absolute path. relativePath: {relativePath}, callerFilePath: {callerFilePath}");
}

/// <inheritdoc />
public IEnumerator BeforeTest(ITest test)
{
var existScenePath = ScenePathFinder.GetExistScenePath(ScenePath);
AsyncOperation loadSceneAsync = null;

if (Application.isEditor)
{
#if UNITY_EDITOR
if (Application.isPlaying)
{
// Play Mode tests running in Editor
loadSceneAsync = EditorSceneManager.LoadSceneAsyncInPlayMode(
existScenePath,
new LoadSceneParameters(LoadSceneMode.Single));
}
else
{
// Edit Mode tests
EditorSceneManager.OpenScene(existScenePath);
}
#endif
}
else
{
// Play Mode tests running on Player
loadSceneAsync = SceneManager.LoadSceneAsync(existScenePath);
}

yield return loadSceneAsync;
// ReSharper disable once ExplicitCallerInfoArgument
yield return SceneManagerHelper.LoadSceneAsync(ScenePath, CallerFilePath);
}

/// <inheritdoc />
Expand Down
3 changes: 0 additions & 3 deletions Runtime/Utils/ScenePathFinder.cs.meta

This file was deleted.

1 change: 1 addition & 0 deletions RuntimeInternals/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
@"This assembly can be used from the runtime code because it does not depend on test-framework.
This assembly is named ""Internal"", however, the included classes are public.")]

[assembly: InternalsVisibleTo("TestHelper.Editor")]
[assembly: InternalsVisibleTo("TestHelper.RuntimeInternals.Tests")]
Loading