Tool windows test suite; drop visual_novel_core dependency
- test_suites/windows (--itest=tools_windows [--shots=<dir>]) opens every tool window, fails on any engine error logged meanwhile, drives the map/interactive/light canvases and can save a screenshot per window. - The tools need the engine mods only; visual_novel_core is no longer a dependency (the Novel Editor collection brings the content).
This commit is contained in:
1 parent
8742aa3da5
commit
55ef9416ef
6 files changed
+445
-1
No files matched your search
@@ -0,0 +1,216 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.Games;
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Menu;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Tools.Hub;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Tests.Windows
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens every registered tool window one by one through the same path the
|
||||
/// toolbar button uses (<see cref="ToolFactory"/> → <see cref="ToolWindow.ShowWindow"/>),
|
||||
/// lets it lay out for a couple of ticks, checks that it entered the tree
|
||||
/// visible and inside the game viewport, optionally saves a screenshot and
|
||||
/// closes it. A window that throws while constructing or laying out fails
|
||||
/// only its own check, the others still run. Ends with the runner contract
|
||||
/// <c>[TEST_RESULT] name=tools_windows passed=N failed=M</c> and the exit code.
|
||||
/// <para/>
|
||||
/// Launched as the <c>novel_editor</c> game (<c>--game=novel_editor --new</c>)
|
||||
/// it first waits for the collection's entry scene, the
|
||||
/// <see cref="NovelEditorHub"/>, and checks it lists a launcher button for
|
||||
/// every registered tool before opening the windows.
|
||||
/// </summary>
|
||||
public partial class ToolWindowsTestDriverNode : Node
|
||||
{
|
||||
private const string ResultName = "tools_windows";
|
||||
private const float TickSeconds = 0.5f;
|
||||
private const float TimeoutSeconds = 90f;
|
||||
private const int SettleTicks = 4;
|
||||
private const int ExerciseTick = 2;
|
||||
|
||||
private readonly string _shotsDir;
|
||||
private readonly List<IToolProvider> _tools = new();
|
||||
private int _passed;
|
||||
private int _failed;
|
||||
private int _toolIndex = -1;
|
||||
private int _settle;
|
||||
private double _elapsed;
|
||||
private bool _finished;
|
||||
private Window _window;
|
||||
private readonly ToolWindowsErrorLogger _errors = new();
|
||||
|
||||
/// <summary>Creates the driver; <paramref name="shotsDir"/> null or empty disables screenshots.</summary>
|
||||
public ToolWindowsTestDriverNode(string shotsDir)
|
||||
{
|
||||
_shotsDir = shotsDir;
|
||||
OS.AddLogger(_errors);
|
||||
var timer = new Timer { WaitTime = TickSeconds, Autostart = true };
|
||||
AddChild(timer);
|
||||
timer.Timeout += Tick;
|
||||
}
|
||||
|
||||
private void Tick()
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
_elapsed += TickSeconds;
|
||||
if (_elapsed >= TimeoutSeconds)
|
||||
{
|
||||
Fail($"timeout ({TimeoutSeconds:0}s) at tool #{_toolIndex}");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_toolIndex < 0)
|
||||
{
|
||||
var tree = Godot.Engine.GetMainLoop() as SceneTree;
|
||||
if (!Boot.IsReady || tree?.CurrentScene == null)
|
||||
return;
|
||||
|
||||
var inHub = ActiveGame.Id == NovelEditorHub.SceneName;
|
||||
if (inHub && tree.CurrentScene is not NovelEditorHub)
|
||||
return;
|
||||
|
||||
_tools.AddRange(ToolFactory.Instance.GetAll());
|
||||
Check(_tools.Count > 0, $"tools registered ({_tools.Count})");
|
||||
if (inHub)
|
||||
CheckHub(tree.CurrentScene);
|
||||
OpenNext();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (--_settle == ExerciseTick)
|
||||
{
|
||||
try
|
||||
{
|
||||
ToolWindowsExercises.Run(_tools[_toolIndex].Id, _window, Check);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Fail($"{_tools[_toolIndex].Id}: interaction threw {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (_settle > 0)
|
||||
return;
|
||||
|
||||
InspectAndClose();
|
||||
OpenNext();
|
||||
}
|
||||
|
||||
private void OpenNext()
|
||||
{
|
||||
_toolIndex++;
|
||||
if (_toolIndex >= _tools.Count)
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
var tool = _tools[_toolIndex];
|
||||
_errors.Drain();
|
||||
try
|
||||
{
|
||||
_window = ToolWindow.ShowWindow(tool.CreateWindow());
|
||||
_settle = SettleTicks;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Fail($"{tool.Id}: open threw {ex.GetType().Name}: {ex.Message}");
|
||||
_window = null;
|
||||
OpenNext();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Every registered tool has its launcher button on the hub screen.</summary>
|
||||
private void CheckHub(Node hub)
|
||||
{
|
||||
var keys = new HashSet<string>();
|
||||
CollectButtonKeys(hub, keys);
|
||||
foreach (var tool in _tools)
|
||||
Check(keys.Contains(tool.LocaleKey), $"hub: launcher button for {tool.Id}");
|
||||
SaveShot("novel_editor_hub");
|
||||
}
|
||||
|
||||
private static void CollectButtonKeys(Node node, HashSet<string> keys)
|
||||
{
|
||||
if (node is Button button && button.HasMeta(MenuBuilder.LocaleMetaKey))
|
||||
keys.Add(button.GetMeta(MenuBuilder.LocaleMetaKey).AsString());
|
||||
foreach (var child in node.GetChildren())
|
||||
CollectButtonKeys(child, keys);
|
||||
}
|
||||
|
||||
private void InspectAndClose()
|
||||
{
|
||||
var id = _tools[_toolIndex].Id;
|
||||
var window = _window;
|
||||
_window = null;
|
||||
|
||||
var errors = _errors.Drain();
|
||||
Check(errors.Count == 0, $"{id}: no engine errors while opening" + (errors.Count > 0 ? ": " + string.Join(" | ", errors) : ""));
|
||||
Check(GodotObject.IsInstanceValid(window) && window.IsInsideTree() && window.Visible, $"{id}: window visible in tree");
|
||||
if (!GodotObject.IsInstanceValid(window))
|
||||
return;
|
||||
|
||||
var viewport = GetTree().Root.GetVisibleRect().Size;
|
||||
Check(window.Size.X <= viewport.X + 1 && window.Size.Y <= viewport.Y + 1,
|
||||
$"{id}: window {window.Size} fits viewport {viewport}");
|
||||
|
||||
SaveShot(id);
|
||||
window.QueueFree();
|
||||
}
|
||||
|
||||
private void SaveShot(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_shotsDir) || DisplayServer.GetName() == "headless")
|
||||
return;
|
||||
|
||||
var dir = ProjectSettings.GlobalizePath(_shotsDir);
|
||||
Directory.CreateDirectory(dir);
|
||||
var image = GetViewport().GetTexture().GetImage();
|
||||
var path = Path.Combine(dir, id + ".png");
|
||||
image.SavePng(path);
|
||||
Log($"screenshot {path}");
|
||||
}
|
||||
|
||||
private void Check(bool ok, string name)
|
||||
{
|
||||
if (ok)
|
||||
{
|
||||
_passed++;
|
||||
Log($"[PASS] {name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Fail(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void Fail(string message)
|
||||
{
|
||||
_failed++;
|
||||
Log($"[FAIL] {message}");
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
_finished = true;
|
||||
OS.RemoveLogger(_errors);
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name={ResultName} passed={_passed} failed={_failed}");
|
||||
GetTree().Quit(_failed == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
private static void Log(string message) => GameLogger.Log("TEST", message);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user