Backgrounds: one locked Background per screen + mini rule (TASK 25)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 25 integration: a pre-consolidation DB (static Background + stray
|
||||
/// "Game Capture" duplicates, wrong flavor on Live, orphaned settings keys)
|
||||
/// heals to the locked model — exactly one Background per screen, index 0,
|
||||
/// right flavor, right name — and a save purges the orphaned keys.
|
||||
/// Real-App + temp-DB pattern (see SourceNamingTests).
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class BackgroundHealIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Dirty_Layout_Heals_To_One_Locked_Background_Per_Scene()
|
||||
{
|
||||
_app.Run(Run);
|
||||
}
|
||||
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public BackgroundHealIntegrationTests(RealAppHost app) => _app = app;
|
||||
|
||||
private void Run()
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-bgheal-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
var window = default(MainWindow);
|
||||
try
|
||||
{
|
||||
using (var schema = new ytLive.Services.LayoutStore(tempDb)) { }
|
||||
SqliteConnection.ClearAllPools();
|
||||
SeedDirtyDatabase(tempDb);
|
||||
|
||||
window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
|
||||
foreach (var name in SceneCatalog.All)
|
||||
{
|
||||
var scene = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, name));
|
||||
Assert.True(scene.HasBackground, $"{name}: HasBackground must be true");
|
||||
|
||||
var background = Assert.Single(
|
||||
scene.Elements.OfType<Source>().Where(s => s.IsBackground).ToList(),
|
||||
s => s.IsBackground);
|
||||
Assert.Equal("Background", background.Name);
|
||||
Assert.Equal(0, scene.Elements.IndexOf(background));
|
||||
|
||||
var wantedType = SceneCatalog.Is(name, SceneCatalog.Live)
|
||||
? SourceType.DisplayCapture
|
||||
: SourceType.Background;
|
||||
Assert.True(background.Type == wantedType,
|
||||
$"{name}: expected {wantedType}, got {background.Type}");
|
||||
}
|
||||
|
||||
vm.SaveLayoutNow();
|
||||
|
||||
using (var connection = new SqliteConnection($"Data Source={tempDb}"))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
using (var dupes = connection.CreateCommand())
|
||||
{
|
||||
dupes.CommandText =
|
||||
"SELECT COUNT(*) FROM Source WHERE Name = 'Game Capture';";
|
||||
Assert.Equal(0L, (long)dupes.ExecuteScalar()!);
|
||||
}
|
||||
|
||||
using (var perScene = connection.CreateCommand())
|
||||
{
|
||||
perScene.CommandText =
|
||||
@"SELECT s.Name, COUNT(*) FROM Source src
|
||||
JOIN Scene s ON s.Id = src.SceneId
|
||||
WHERE src.IsBackground = 1 GROUP BY s.Name;";
|
||||
using var reader = perScene.ExecuteReader();
|
||||
var counts = new System.Collections.Generic.Dictionary<string, long>();
|
||||
while (reader.Read())
|
||||
counts[reader.GetString(0)] = reader.GetInt64(1);
|
||||
var live = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live));
|
||||
vm.StagedScene = live;
|
||||
|
||||
// Layering invariant: the Live screen's background is capture-flavored,
|
||||
// so it must never feed ActiveBackgroundImage (that image renders above
|
||||
// the capture and would cover it permanently).
|
||||
Assert.Null(vm.ActiveBackgroundImage);
|
||||
|
||||
foreach (var name in SceneCatalog.All)
|
||||
Assert.True(counts.TryGetValue(name, out var c) && c == 1,
|
||||
$"{name}: expected exactly 1 background row");
|
||||
}
|
||||
|
||||
using (var staleKeys = connection.CreateCommand())
|
||||
{
|
||||
staleKeys.CommandText =
|
||||
@"SELECT COUNT(*) FROM Settings
|
||||
WHERE (Key LIKE 'BackgroundUseDefault\_%' ESCAPE '\'
|
||||
OR Key LIKE 'BackgroundPath\_%' ESCAPE '\')
|
||||
AND SUBSTR(Key, INSTR(Key, '_') + 1) IN ('bg-stray-1','bg-stray-2');";
|
||||
Assert.Equal(0L, (long)staleKeys.ExecuteScalar()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
window?.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Recreates the rot observed in the user's real DB: duplicate
|
||||
/// capture-flavored rows beside static ones, a misnamed Live background,
|
||||
/// and settings keys pointing at rows that shouldn't exist.</summary>
|
||||
private static void SeedDirtyDatabase(string path)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path}");
|
||||
connection.Open();
|
||||
|
||||
var scenes = new (string Id, string Name)[]
|
||||
{
|
||||
("scene-starting", "Starting"),
|
||||
("scene-live", "Live"),
|
||||
("scene-brb", "BRB"),
|
||||
("scene-chat", "Chat"),
|
||||
("scene-ending", "Ending"),
|
||||
};
|
||||
foreach (var (id, name) in scenes)
|
||||
{
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"INSERT INTO Scene (Id, Name, HasBackground, SortOrder) VALUES ($id, $name, 1, $order);";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$name", name);
|
||||
cmd.Parameters.AddWithValue("$order", Array.IndexOf(scenes, (id, name)));
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
InsertSource(connection, "bg-static-1", "scene-starting", "Background", "Background", 0);
|
||||
InsertSource(connection, "bg-stray-1", "scene-starting", "Game Capture", "DisplayCapture", 1);
|
||||
InsertSource(connection, "bg-live-1", "scene-live", "Game Capture", "DisplayCapture", 0);
|
||||
InsertSource(connection, "bg-static-2", "scene-chat", "Background", "Background", 0);
|
||||
InsertSource(connection, "bg-stray-2", "scene-chat", "Game Capture", "DisplayCapture", 1);
|
||||
|
||||
using (var key1 = connection.CreateCommand())
|
||||
{
|
||||
key1.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('BackgroundUseDefault_bg-stray-1', '0');";
|
||||
key1.ExecuteNonQuery();
|
||||
}
|
||||
using (var key2 = connection.CreateCommand())
|
||||
{
|
||||
key2.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('BackgroundPath_bg-stray-2', 'C:\\\\nope.png');";
|
||||
key2.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
private static void InsertSource(
|
||||
SqliteConnection connection, string id, string sceneId,
|
||||
string name, string type, int sortOrder)
|
||||
{
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
@"INSERT INTO Source (Id, SceneId, Type, Name, IsEnabled, X, Y, Width, Height, IsBackground, SortOrder)
|
||||
VALUES ($id, $sceneId, $type, $name, 1, 0, 0, 1920, 1080, 1, $sortOrder);";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$sceneId", sceneId);
|
||||
cmd.Parameters.AddWithValue("$type", type);
|
||||
cmd.Parameters.AddWithValue("$name", name);
|
||||
cmd.Parameters.AddWithValue("$sortOrder", sortOrder);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user