Skip to content

Commit

Permalink
Add end-to-end test framework inspired from iOS interactive tests
Browse files Browse the repository at this point in the history
This brings interactive tests from iOS and allows them to be run in any environment.
Specifically, given a set of `Game` classes under `Tests/Interactive/Tests`, we can run that `Game` instance in a test harness under DesktopGL (Mac/Windows)/iOS (Android coming next).

- Lots of reshuffling from Tests/Interactive/iOS -> Common/Tests
  - Also added doc to run the tests in simulator/on-device.
- New TestRunners (for now DesktopGL on Mac/Windows and iOS)- will add Android next
  - DesktopGL only allows running one test per run, due to STA/SDL Window event loop.
  - Perhaps consider NUnit test that launches the DesktopGL in a separate process with the correct command-line parameter?
- Added doc in Tests/Interactive/README.md on how to run the test runners.
- Allows 'automatic' running of InteractiveTests too (i.e. automatically run one test, allow test to signal 'finish' and exit)
  - i.e. provided a `TestGame` that implements `Game` and allows a test author to signal when the test is done. (i.e. either by clicking the Exit button or the test can run automatically, then signal Exit)
  • Loading branch information
Mindfulplays committed Dec 29, 2023
1 parent 5fc1d04 commit 5203bc3
Show file tree
Hide file tree
Showing 60 changed files with 2,484 additions and 2,471 deletions.
13 changes: 0 additions & 13 deletions Tests/Directory.Build.props

This file was deleted.

19 changes: 19 additions & 0 deletions Tests/Interactive/Common/Categories.cs
@@ -0,0 +1,19 @@
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

namespace MonoGame.InteractiveTests
{
/// <summary>
/// Defines test app categories that is provided via a <code>class</code>
/// attribute as well as a human-readable string shown in a UI or provided
/// via command line arg.
/// </summary>
static class Categories
{
public const string Default = General;

public const string General = "General";
public const string Meta = "Meta Tests";
}
}
52 changes: 52 additions & 0 deletions Tests/Interactive/Common/GameDebug.cs
@@ -0,0 +1,52 @@
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

using System.Collections.Generic;

namespace MonoGame.InteractiveTests
{
/// <summary>
/// Allows tests to output console messages including spammy messages that
/// may be throttled.
///
/// On various platforms, this may be available via console output or via
/// a special console viewer (Console app on Mac; `adb logcat` on Android etc).
/// </summary>
public partial class GameDebug
{
/// <summary>Output a single console message.</summary>
public static void C(string message)
{
System.Console.WriteLine($"MGDBG: {message}");
}

/// <summary>Output an error message to the console.</summary>
public static void E(string message)
{
System.Console.WriteLine($"****ERROR*****:MGDBG: {message}");
}

/// <summary>Maintains the spam message counts to prevent spamming the console.</summary>
private record MessageCount(int Count)
{
public int Count { get; set; } = Count;
}
private static readonly Dictionary<string, MessageCount> MESSAGES_COUNTS_ = new();

/// <summary>Use this to output spammy messages.</summary>
public static void Spam(string message, int maxNumTimes = 10)
{
if (!MESSAGES_COUNTS_.TryGetValue(message, out var numTimes))
{
numTimes = new(0);
MESSAGES_COUNTS_.Add(message, numTimes);
}

if (numTimes.Count >= maxNumTimes) { return; }

System.Console.WriteLine($"MGDBG: {message} ...#{numTimes.Count}");
numTimes.Count++;
}
}
}
70 changes: 70 additions & 0 deletions Tests/Interactive/Common/InteractiveTest.cs
@@ -0,0 +1,70 @@
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using MonoGame.Framework.Utilities;

namespace MonoGame.InteractiveTests
{
/// <summary>
/// Manages creating of interactive test(s) instance(s).
/// </summary>
public class InteractiveTest
{
public static bool TryCreateFrom(Type type, out InteractiveTest test)
{
test = null;
if (!typeof(TestGame).IsAssignableFrom(type)) { return false; }

var attrs = type.GetCustomAttributes(typeof(InteractiveTestAttribute), false);
if (attrs.Length == 0) { return false; }

var attr = (InteractiveTestAttribute)attrs[0];

test = new InteractiveTest(
type, attr.Name ?? type.Name, attr.Category ?? Categories.Default, attr.Platforms);
return true;
}

private InteractiveTest(Type type, string name, string category, MonoGamePlatform[] platforms)
{
_type = type;
_name = name;
_category = category;
_platforms = platforms;
}

private readonly Type _type;
public Type Type { get { return _type; } }

private readonly string _name;
public string Name { get { return _name; } }

private readonly string _category;
public string Category { get { return _category; } }

private readonly MonoGamePlatform[] _platforms;
public MonoGamePlatform[] Platforms { get { return _platforms; } }

public Game Create()
{
return (Game)Activator.CreateInstance(_type);
}

public bool MatchesPlatform(MonoGamePlatform runtimePlatform)
{
// Empty array matches everything.
if (_platforms.Length == 0) { return true; }

foreach (var testPlatform in _platforms)
{
if (testPlatform == runtimePlatform) { return true; }
}

return false;
}
}
}
48 changes: 48 additions & 0 deletions Tests/Interactive/Common/InteractiveTestAttribute.cs
@@ -0,0 +1,48 @@
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

using System;
using System.Collections.Generic;
using MonoGame.Framework.Utilities;

namespace MonoGame.InteractiveTests
{
/// <summary>
/// Attribute specified on test classes that are automatically
/// discovered and shown/provided via some UI / command-line arg.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class InteractiveTestAttribute : Attribute
{
public InteractiveTestAttribute(string name, string category,
MonoGamePlatform[] platforms = null)
{
_name = name;
_category = category;

// Empty array matches everything.
if (platforms == null) { platforms = new MonoGamePlatform[] { }; }

_platforms = platforms;
}

/// <summary>Human-readable name of the test</summary>
private readonly string _name;

public string Name { get { return _name; } }

/// <summary>Category of the test. See <see cref="Categories"/></summary>
private readonly string _category;

public string Category { get { return _category; } }

/// <summary>
/// Supported platforms that this test can run on (empty array
/// allows running on any platform.
/// </summary>
private readonly MonoGamePlatform[] _platforms;

public MonoGamePlatform[] Platforms { get { return _platforms; } }
}
}
65 changes: 65 additions & 0 deletions Tests/Interactive/Common/InteractiveTests.cs
@@ -0,0 +1,65 @@
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

using System.Collections.Generic;
using System.Reflection;
using MonoGame.Framework.Utilities;

namespace MonoGame.InteractiveTests
{
/// <summary>
/// Creates a <see cref="InteractiveTest"/> from applicable types in our binary.
/// Also allows filtering of tests based on platforms/command-line args.
/// </summary>
public class InteractiveTests
{
private readonly List<InteractiveTest> _interactiveTests = new();

private readonly List<InteractiveTest> _filteredTests = new();

public InteractiveTests()
{
_interactiveTests.Clear();
var assembly = Assembly.GetExecutingAssembly();
foreach (var type in assembly.GetTypes())
{
InteractiveTest test;
if (!InteractiveTest.TryCreateFrom(type, out test)) { continue; }

if (test.MatchesPlatform(PlatformInfo.MonoGamePlatform)) { _interactiveTests.Add(test); }
}
GameDebug.C($"--Discovered {_interactiveTests.Count} tests.");
}

public IReadOnlyList<InteractiveTest> Tests { get { return _interactiveTests; } }

/// <summary>
/// Parses the passed-in arg and returns an `InteractiveTest` game. See HelpStr for more details.
/// </summary>
public IReadOnlyList<InteractiveTest> Parse(string[] args)
{
_filteredTests.Clear();
if (args == null || args.Length == 0) { return _filteredTests; }
foreach (var test in _interactiveTests)
{
foreach (var testName in args)
{
if (test.Name.ToLower().Contains(testName.ToLower())) { _filteredTests.Add(test); }
}
}

return _filteredTests;
}

public string HelpStr()
{
var testStr = "";
foreach (var test in _interactiveTests) { testStr += $"{test.Name}\n"; }

return $@"Interactive tests available:
{testStr}
";
}
}
}
13 changes: 13 additions & 0 deletions Tests/Interactive/Common/MonoGame.Interactive.Common.projitems
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>MonoGame</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)\**\*.cs" />
</ItemGroup>
</Project>
11 changes: 11 additions & 0 deletions Tests/Interactive/Common/MonoGame.Interactive.Common.shproj
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>085B407C-726C-43FE-BB55-64E2B6D143FE</ProjectGuid>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
<Import Project="MonoGame.Interactive.Common.projitems" Label="Shared" />
</Project>

0 comments on commit 5203bc3

Please sign in to comment.