Skip to content

Commit

Permalink
Merge pull request #73 from nowsprinting/feature/buildscene_attribute
Browse files Browse the repository at this point in the history
Add BuildScene attribute and LoadSceneAsync method
  • Loading branch information
nowsprinting authored Apr 29, 2024
2 parents 5519b78 + 064edf7 commit fad6d14
Show file tree
Hide file tree
Showing 22 changed files with 1,789 additions and 135 deletions.
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

0 comments on commit fad6d14

Please sign in to comment.