From ceaa6cb8df8af46d253592a4543dcd258a945998 Mon Sep 17 00:00:00 2001 From: Markus Palcer Date: Mon, 29 Dec 2025 16:41:32 +0100 Subject: [PATCH 1/2] rework macro action command and ensure its serialization is tested closes #74 --- .../DeserializedCommandGenerator.cs | 15 ++-- AtemSharp.CodeGenerators/Helpers.cs | 3 + .../SerializedCommandGenerator.cs | 21 +++-- .../State/StateGenerator.cs | 6 +- AtemSharp.Tests/AtemSwitcherTests.cs | 29 +++---- .../Commands/Macro/MacroActionCommandTests.cs | 11 ++- .../Macro/MacroRunStatusUpdateCommandTests.cs | 2 +- .../Communication/AtemClientTests.cs | 43 +++------ .../Communication/AtemProtocolTests.cs | 3 +- AtemSharp.Tests/State/MacroSystemTests.cs | 87 +++++++++---------- .../Commands/Macro/MacroActionCommand.cs | 24 ++--- .../Macro/MacroRunStatusUpdateCommand.cs | 2 +- AtemSharp/Extensions/VideoModeExtensions.cs | 70 +++++++-------- AtemSharp/State/Macro/Macro.cs | 40 ++++++++- AtemSharp/State/Macro/MacroPlayer.cs | 34 ++++++-- AtemSharp/State/Macro/MacroRecorder.cs | 40 +++++++-- stryker-config.json | 2 +- 17 files changed, 256 insertions(+), 176 deletions(-) diff --git a/AtemSharp.CodeGenerators/Deserialization/DeserializedCommandGenerator.cs b/AtemSharp.CodeGenerators/Deserialization/DeserializedCommandGenerator.cs index ac8c643b..58928d1d 100644 --- a/AtemSharp.CodeGenerators/Deserialization/DeserializedCommandGenerator.cs +++ b/AtemSharp.CodeGenerators/Deserialization/DeserializedCommandGenerator.cs @@ -46,18 +46,19 @@ protected override void ProcessClass(SourceProductionContext spc, INamedTypeSymb return; } - var namespaces = new[] { + var namespaces = new[] + { "using System;", "using System.Diagnostics.CodeAnalysis;", "using AtemSharp;", "using AtemSharp.State.Info;", - "using AtemSharp.Lib;" + "using AtemSharp.Lib;", + "using System.CodeDom.Compiler;" }.Concat(fields.Select(x => x!.NamespaceCode)).Distinct(); var internalDeserialization = GetInternalDeserializationMethod(classDecl); - var fileContent = $$""" {{string.Join("\n", namespaces)}} @@ -69,6 +70,7 @@ namespace {{ns}}; { {{string.Join("\n", fields.Select(x => x!.PropertyCode))}} + {{Helpers.CodeGeneratorAttribute}} public static IDeserializedCommand Deserialize(ReadOnlySpan rawCommand, ProtocolVersion protocolVersion) { var result = new {{className}} { @@ -79,6 +81,7 @@ public static IDeserializedCommand Deserialize(ReadOnlySpan rawCommand, Pr return result; } } + #nullable restore """; @@ -100,7 +103,8 @@ private static string GetInternalDeserializationMethod(ClassDeclarationSyntax cl continue; } - if (!method.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword) || m.IsKind(SyntaxKind.PrivateKeyword) || m.IsKind(SyntaxKind.InternalKeyword))) + if (!method.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword) || m.IsKind(SyntaxKind.PrivateKeyword) || + m.IsKind(SyntaxKind.InternalKeyword))) { continue; } @@ -121,7 +125,6 @@ private static string GetInternalDeserializationMethod(ClassDeclarationSyntax cl { return "result.DeserializeInternal(rawCommand, protocolVersion);"; } - } return string.Empty; @@ -140,7 +143,7 @@ private static string CreatePropertyCode(IFieldSymbol f) return $$""" {{Helpers.GetFieldMsDocComment(f)}} - [ExcludeFromCodeCoverage] + {{Helpers.CodeGeneratorAttribute}} public {{fieldType}} {{propertyName}} { get => {{f.Name}}; diff --git a/AtemSharp.CodeGenerators/Helpers.cs b/AtemSharp.CodeGenerators/Helpers.cs index ce579077..852ff87b 100644 --- a/AtemSharp.CodeGenerators/Helpers.cs +++ b/AtemSharp.CodeGenerators/Helpers.cs @@ -349,5 +349,8 @@ public static string CreateNamespaceCode(IFieldSymbol fieldSymbol) return $"using {ns};"; } + + public static string CodeGeneratorAttribute => + $"[GeneratedCode(\"{typeof(Helpers).Assembly.FullName}\",\"{typeof(Helpers).Assembly.ImageRuntimeVersion}\")]"; } } diff --git a/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs b/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs index d577e7a1..41915131 100644 --- a/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs +++ b/AtemSharp.CodeGenerators/Serialization/SerializedCommandGenerator.cs @@ -56,7 +56,8 @@ protected override void ProcessClass(SourceProductionContext spc, INamedTypeSymb "using System.Diagnostics.CodeAnalysis;", "using AtemSharp;", "using AtemSharp.State.Info;", - "using static AtemSharp.Commands.SerializationExtensions;" + "using static AtemSharp.Commands.SerializationExtensions;", + "using System.CodeDom.Compiler;" }.Concat(fields.Select(x => x!.NamespaceCode)).Distinct(); var fileContent = $$""" @@ -71,6 +72,7 @@ public partial class {{className}} {{string.Join("\n", fields.Select(x => x!.PropertyCode))}} /// + {{Helpers.CodeGeneratorAttribute}} public override byte[] Serialize(ProtocolVersion version) { var commandVersion = typeof({{className}}).GetCustomAttribute()?.MinimumVersion; if (commandVersion is not null && commandVersion < version) { @@ -87,6 +89,7 @@ public override byte[] Serialize(ProtocolVersion version) { return buffer; } } + #nullable restore """; @@ -130,18 +133,18 @@ private static string GetPropertyCode(IFieldSymbol f) var setter = f.IsReadOnly ? string.Empty : $$""" - set { - {{validationCode}} - {{f.Name}} = value; - {{flagCode}} - } - """; + set { + {{validationCode}} + {{f.Name}} = value; + {{flagCode}} + } + """; return $$""" {{docComment}} + {{Helpers.CodeGeneratorAttribute}} {{visibility}} {{fieldType}} {{propertyName}} { - [ExcludeFromCodeCoverage] get => {{f.Name}}; {{setter}} } @@ -157,7 +160,7 @@ private static string GetPropertyCode(IFieldSymbol f) var offset = Helpers.GetSerializationOffest(f); var fieldType = Helpers.GetFieldType(f); - var isDouble = fieldType == "double" || fieldType == "System.Double"; + var isDouble = fieldType is "double" or "System.Double"; var extensionMethod = Helpers.GetSerializationMethod(f); var scalingFactor = Helpers.GetScalingFactor(f); diff --git a/AtemSharp.CodeGenerators/State/StateGenerator.cs b/AtemSharp.CodeGenerators/State/StateGenerator.cs index 59e58f4a..fe7b358d 100644 --- a/AtemSharp.CodeGenerators/State/StateGenerator.cs +++ b/AtemSharp.CodeGenerators/State/StateGenerator.cs @@ -11,7 +11,8 @@ public class StateGenerator : CodeGeneratorBase private static readonly string[] HardCodedNamespaces = { "System.ComponentModel", - "System.Runtime.CompilerServices" + "System.Runtime.CompilerServices", + "System.CodeDom.Compiler" }; protected override void ProcessClass(SourceProductionContext spc, INamedTypeSymbol classSymbol, ClassDeclarationSyntax classDecl) @@ -43,6 +44,7 @@ partial class {{classSymbol.Name}} : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; + {{Helpers.CodeGeneratorAttribute}} protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); @@ -71,11 +73,13 @@ private ProcessedField ProcessField(IFieldSymbol field) var sendUpdateDeclaration = isReadOnly ? string.Empty : $"private partial void Send{propertyName}UpdateCommand({type} value);"; var code = $$""" + {{Helpers.CodeGeneratorAttribute}} public {{type}} {{propertyName}} { get => {{field.Name}}; {{setterCode}} } + {{Helpers.CodeGeneratorAttribute}} internal void Update{{propertyName}}({{type}} value) { {{field.Name}} = value; {{propertyName}}Changed?.Invoke(this, EventArgs.Empty); diff --git a/AtemSharp.Tests/AtemSwitcherTests.cs b/AtemSharp.Tests/AtemSwitcherTests.cs index 6c33196c..ae6b8e45 100644 --- a/AtemSharp.Tests/AtemSwitcherTests.cs +++ b/AtemSharp.Tests/AtemSwitcherTests.cs @@ -2,7 +2,6 @@ using AtemSharp.Commands.Macro; using AtemSharp.Commands.MixEffects.Transition; using AtemSharp.Lib; -using AtemSharp.State.Macro; using AtemSharp.State.Video.MixEffect; using AtemSharp.Tests.TestUtilities; @@ -15,7 +14,7 @@ public class AtemSwitcherTests private class TestData : IAsyncDisposable { public AtemSwitcher Atem; - public TestServices Services; + public readonly TestServices Services; public TestData() { @@ -84,8 +83,7 @@ public async Task ConnectAsync_WhileConnected_ShouldThrow() data.Services.ClientFake.SimulateReceivedCommand(new InitCompleteCommand()); await data.Atem.ConnectAsync().WithTimeout(); - var ex = Assert.ThrowsAsync(() => data.Atem.ConnectAsync()); - Assert.That(ex!.Message, Does.Contain("Can not connect while")); + Assert.ThrowsAsync(() => data.Atem.ConnectAsync()); } [Test] @@ -95,8 +93,7 @@ public async Task ConnectAsync_WhileConnecting_ShouldThrow() _ = data.Atem.ConnectAsync(); - var ex = Assert.ThrowsAsync(async () => await data.Atem.ConnectAsync().WithTimeout()); - Assert.That(ex!.Message, Does.Contain("Can not connect while")); + Assert.ThrowsAsync(async () => await data.Atem.ConnectAsync().WithTimeout()); } [Test] @@ -187,8 +184,7 @@ public async Task DisconnectAsync_WhileConnecting_ShouldThrow() await using var data = new TestData(); _ = data.Atem.ConnectAsync(); - var ex = Assert.ThrowsAsync(() => data.Atem.DisconnectAsync().WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while transitioning connection states")); + Assert.ThrowsAsync(() => data.Atem.DisconnectAsync().WithTimeout()); Assert.That(data.Atem.ConnectionState, Is.EqualTo(ConnectionState.Connecting)); } @@ -204,8 +200,7 @@ public async Task DisconnectAsync_WhileDisconnecting_ShouldThrow() var disconnectTask = data.Atem.DisconnectAsync(); Assert.That(disconnectTask.IsCompleted, Is.False); - var ex = Assert.ThrowsAsync(() => data.Atem.DisconnectAsync().WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while transitioning connection states")); + Assert.ThrowsAsync(() => data.Atem.DisconnectAsync().WithTimeout()); } [Test] @@ -255,9 +250,8 @@ public async Task SendCommandAsync_WhileNotConnected_ShouldThrow() { await using var data = new TestData(); - var sendTask = data.Atem.SendCommandAsync(new MacroActionCommand(new Macro(data.Atem), MacroAction.Run)); - var ex = Assert.ThrowsAsync(async () => await sendTask.WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while not connected")); + var sendTask = data.Atem.SendCommandAsync(MacroActionCommand.Stop()); + Assert.ThrowsAsync(async () => await sendTask.WithTimeout()); } [Test] @@ -269,7 +263,7 @@ public async Task SendCommandsAsync() data.Services.ClientFake.SimulateReceivedCommand(new InitCompleteCommand()); await data.Atem.ConnectAsync().WithTimeout(); - MacroActionCommand[] commands = [new(new Macro(data.Atem), MacroAction.Run), new(new Macro(data.Atem), MacroAction.Stop)]; + MacroActionCommand[] commands = [MacroActionCommand.Stop(), MacroActionCommand.Continue()]; await data.Atem.SendCommandsAsync(commands).WithTimeout(); Assert.That(data.Services.ClientFake.SentCommands, Is.EquivalentTo(commands)); @@ -280,10 +274,9 @@ public async Task SendCommandsAsync_WhileNotConnected_ShouldThrow() { await using var data = new TestData(); - MacroActionCommand[] commands = [new(new Macro(data.Atem), MacroAction.Run), new(new Macro(data.Atem), MacroAction.Stop)]; + MacroActionCommand[] commands = [MacroActionCommand.Stop(), MacroActionCommand.Continue()]; var sendTask = data.Atem.SendCommandsAsync(commands); - var ex = Assert.ThrowsAsync(async () => await sendTask.WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while not connected")); + Assert.ThrowsAsync(async () => await sendTask.WithTimeout()); } [Test] @@ -321,7 +314,7 @@ public async Task ReceiveCommand_ShouldUpdateState() { Assert.That(data.Atem.Macros.Player.CurrentlyPlaying, Is.SameAs(data.Atem.Macros[2])); Assert.That(data.Atem.Macros.Player.PlayLooped, Is.True); - Assert.That(data.Atem.Macros.Player.PlaybackIsWaiting, Is.False); + Assert.That(data.Atem.Macros.Player.PlaybackIsWaitingForUserAction, Is.False); }); } diff --git a/AtemSharp.Tests/Commands/Macro/MacroActionCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroActionCommandTests.cs index cdd171bf..dde7c3ec 100644 --- a/AtemSharp.Tests/Commands/Macro/MacroActionCommandTests.cs +++ b/AtemSharp.Tests/Commands/Macro/MacroActionCommandTests.cs @@ -15,6 +15,15 @@ public class CommandData : CommandDataBase protected override MacroActionCommand CreateSut(TestUtilities.CommandTests.TestCaseData testCase) { var macro = new AtemSharp.State.Macro.Macro(Substitute.For()) { Id = testCase.Command.Index }; - return new MacroActionCommand(macro, testCase.Command.Action); + return testCase.Command.Action switch + { + MacroAction.Run => MacroActionCommand.Run(macro), + MacroAction.Stop => MacroActionCommand.Stop(), + MacroAction.StopRecord => MacroActionCommand.StopRecord(), + MacroAction.InsertUserWait => MacroActionCommand.InsertUserWait(), + MacroAction.Continue => MacroActionCommand.Continue(), + MacroAction.Delete => MacroActionCommand.Delete(macro), + _ => throw new ArgumentOutOfRangeException() + }; } } diff --git a/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs b/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs index 7cd592e8..df4d176d 100644 --- a/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs +++ b/AtemSharp.Tests/Commands/Macro/MacroRunStatusUpdateCommandTests.cs @@ -44,6 +44,6 @@ protected override void CompareStateProperties(IStateHolder state, CommandData e } Assert.That(state.Macros.Player.PlayLooped, Is.EqualTo(expectedData.Loop)); - Assert.That(state.Macros.Player.PlaybackIsWaiting, Is.EqualTo(expectedData.IsWaiting)); + Assert.That(state.Macros.Player.PlaybackIsWaitingForUserAction, Is.EqualTo(expectedData.IsWaiting)); } } diff --git a/AtemSharp.Tests/Communication/AtemClientTests.cs b/AtemSharp.Tests/Communication/AtemClientTests.cs index c5267b31..b4165e17 100644 --- a/AtemSharp.Tests/Communication/AtemClientTests.cs +++ b/AtemSharp.Tests/Communication/AtemClientTests.cs @@ -5,7 +5,6 @@ using AtemSharp.Commands.Macro; using AtemSharp.Communication; using AtemSharp.Lib; -using AtemSharp.State.Macro; using AtemSharp.Tests.TestUtilities; using NSubstitute; @@ -75,13 +74,11 @@ public async Task Connect_WhileConnecting() await using var data = new TestInstances(); var connectTask = data.Sut.ConnectAsync("127.0.0.1", 12345); - var ex = Assert.ThrowsAsync(async () => await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout()); + Assert.ThrowsAsync(async () => await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout()); data.Services.ProtocolFake.SucceedConnection(); await connectTask.WithTimeout(); - - Assert.That(ex.Message, Contains.Substring("while already connecting")); } [Test] @@ -91,8 +88,7 @@ public async Task Connect_WhileConnected() data.Services.ProtocolFake.SucceedConnection(); await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout(); - var ex = Assert.ThrowsAsync(async () => await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while already connected")); + Assert.ThrowsAsync(async () => await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout()); } [Test] @@ -104,12 +100,10 @@ public async Task Connect_WhileDisconnecting() var disconnectTask = data.Sut.DisconnectAsync(); - var ex = Assert.ThrowsAsync(async () => await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout()); + Assert.ThrowsAsync(async () => await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout()); data.Services.ProtocolFake.SucceedDisconnection(); await disconnectTask.WithTimeout(); - - Assert.That(ex.Message, Contains.Substring("while disconnecting")); } [Test] @@ -142,7 +136,7 @@ public async Task Disconnect_CancelsSendTasks() data.Services.ProtocolFake.SucceedConnection(); await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout(); - var sendTask = data.Sut.SendCommandAsync(new MacroActionCommand(new Macro(Substitute.For()), MacroAction.Run)); + var sendTask = data.Sut.SendCommandAsync(MacroActionCommand.Stop()); data.Services.ProtocolFake.SucceedDisconnection(); await data.Sut.DisconnectAsync().WithTimeout(); @@ -158,8 +152,7 @@ public async Task Disconnect_WhileConnecting() _ = data.Sut.ConnectAsync("127.0.0.1", 12345); await data.Services.ProtocolFake.WaitForConnectionRequestAsync().WithTimeout(); - var ex = Assert.ThrowsAsync(async () => await data.Sut.DisconnectAsync().WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while connecting")); + Assert.ThrowsAsync(async () => await data.Sut.DisconnectAsync().WithTimeout()); } [Test] @@ -167,8 +160,7 @@ public async Task Disconnect_WhileNotConnected() { await using var data = new TestInstances(); - var ex = Assert.ThrowsAsync(async () => await data.Sut.DisconnectAsync().WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while not connected")); + Assert.ThrowsAsync(async () => await data.Sut.DisconnectAsync().WithTimeout()); } [Test] @@ -182,8 +174,7 @@ public async Task Disconnect_WhileDisconnecting() var disconnectTask = data.Sut.DisconnectAsync(); await data.Services.ProtocolFake.WaitForDisconnectionRequestAsync().WithTimeout(); - var ex = Assert.ThrowsAsync(async () => await data.Sut.DisconnectAsync().WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while already disconnecting")); + Assert.ThrowsAsync(async () => await data.Sut.DisconnectAsync().WithTimeout()); data.Services.ProtocolFake.SucceedDisconnection(); await disconnectTask.WithTimeout(); @@ -213,8 +204,7 @@ public async Task SendCommand() data.Services.ProtocolFake.SucceedConnection(); await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout(); - var command = new MacroActionCommand(new Macro(Substitute.For()) { Id = 0 }, MacroAction.Run); - var sendTask = data.Sut.SendCommandAsync(command); + var sendTask = data.Sut.SendCommandAsync(MacroActionCommand.Stop()); var packet = await data.Services.ProtocolFake.GetSentPacket().WithTimeout(); VerifySentPacket(packet, expectedPayload); @@ -234,11 +224,10 @@ public async Task SendCommand_Twice() data.Services.ProtocolFake.SucceedConnection(); await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout(); - var command = new MacroActionCommand(new Macro(Substitute.For()) { Id = 0 }, MacroAction.Run); Task[] sendTasks = [ - data.Sut.SendCommandAsync(command), - data.Sut.SendCommandAsync(command) + data.Sut.SendCommandAsync(MacroActionCommand.Stop()), + data.Sut.SendCommandAsync(MacroActionCommand.Stop()) ]; var packet = await data.Services.ProtocolFake.GetSentPacket().WithTimeout(); @@ -260,8 +249,7 @@ public async Task SendCommands_Two() data.Services.ProtocolFake.SucceedConnection(); await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout(); - var command = new MacroActionCommand(new Macro(Substitute.For()) { Id = 0 }, MacroAction.Run); - var sendTask = data.Sut.SendCommandsAsync([command, command]); + var sendTask = data.Sut.SendCommandsAsync([MacroActionCommand.Stop(), MacroActionCommand.Stop()]); var packet = await data.Services.ProtocolFake.GetSentPacket().WithTimeout(); @@ -293,8 +281,7 @@ public async Task AckingUnsentCommand_DoesNotPoseAProblem() await data.Sut.ConnectAsync("127.0.0.1", 12345).WithTimeout(); - var command = new MacroActionCommand(new Macro(Substitute.For()) { Id = 0 }, MacroAction.Run); - var sendTask = data.Sut.SendCommandAsync(command); + var sendTask = data.Sut.SendCommandAsync(MacroActionCommand.Stop()); var packet = await data.Services.ProtocolFake.GetSentPacket().WithTimeout(); VerifySentPacket(packet, expectedPayload); @@ -312,11 +299,9 @@ public async Task SendCommand_WhileNotConnected() { await using var data = new TestInstances(); - var command = new MacroActionCommand(new Macro(Substitute.For()) { Id = 0 }, MacroAction.Run); - var sendTask = data.Sut.SendCommandAsync(command).WithTimeout(); + var sendTask = data.Sut.SendCommandAsync(MacroActionCommand.Stop()).WithTimeout(); - var ex = Assert.ThrowsAsync(async () => await sendTask.WithTimeout()); - Assert.That(ex.Message, Contains.Substring("while not connected")); + Assert.ThrowsAsync(async () => await sendTask.WithTimeout()); } [Test] diff --git a/AtemSharp.Tests/Communication/AtemProtocolTests.cs b/AtemSharp.Tests/Communication/AtemProtocolTests.cs index 45c3f9bc..e918f012 100644 --- a/AtemSharp.Tests/Communication/AtemProtocolTests.cs +++ b/AtemSharp.Tests/Communication/AtemProtocolTests.cs @@ -58,8 +58,7 @@ public async Task Connect_WhileConnected() await services.VirtualTime.AdvanceBy(TimeSpan.FromMilliseconds(1)); - var ex = Assert.ThrowsAsync(() => sut.ConnectAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9110)).WithTimeout()); - Assert.That(ex.Message, Contains.Substring("Can only connect while not connected")); + Assert.ThrowsAsync(() => sut.ConnectAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9110)).WithTimeout()); } [Test] diff --git a/AtemSharp.Tests/State/MacroSystemTests.cs b/AtemSharp.Tests/State/MacroSystemTests.cs index fd34bfb8..36312ac1 100644 --- a/AtemSharp.Tests/State/MacroSystemTests.cs +++ b/AtemSharp.Tests/State/MacroSystemTests.cs @@ -88,25 +88,7 @@ public async Task StopMacro() Assert.That(switcher.SentCommands, Has.Count.EqualTo(1)); var command = switcher.SentCommands[0].As(); - Assert.Multiple(() => - { - Assert.That(command.Index, Is.EqualTo(2)); - Assert.That(command.Action, Is.EqualTo(MacroAction.Stop)); - }); - } - - [Test] - public async Task StopNonRunningMacro() - { - var switcher = new AtemSwitcherFake(); - var sut = new MacroSystem(switcher); - sut.Populate(5); - - sut.Player.UpdateCurrentlyPlaying(null); - - await sut.Player.StopPlayback().WithTimeout(); - - Assert.That(switcher.SentCommands, Has.Count.EqualTo(0)); + Assert.That(command.Action, Is.EqualTo(MacroAction.Stop)); } [Test] @@ -122,25 +104,7 @@ public async Task StopMacroRecord() Assert.That(switcher.SentCommands, Has.Count.EqualTo(1)); var command = switcher.SentCommands[0].As(); - Assert.Multiple(() => - { - Assert.That(command.Index, Is.EqualTo(2)); - Assert.That(command.Action, Is.EqualTo(MacroAction.StopRecord)); - }); - } - - [Test] - public async Task StopNonRecordingMacro() - { - var switcher = new AtemSwitcherFake(); - var sut = new MacroSystem(switcher); - sut.Populate(5); - - sut.Recorder.UpdateCurrentlyRecording(null); - - await sut.Recorder.StopRecording().WithTimeout(); - - Assert.That(switcher.SentCommands, Has.Count.EqualTo(0)); + Assert.That(command.Action, Is.EqualTo(MacroAction.StopRecord)); } [Test] @@ -166,12 +130,8 @@ public void AddPauseWhileNotRecording() sut.Populate(5); sut.Recorder.UpdateCurrentlyRecording(null); - var ex = Assert.ThrowsAsync(() => sut.Recorder.AddPause(12).WithTimeout()); - Assert.Multiple(() => - { - Assert.That(ex.Message, Does.Contain("Can only add pause while recording")); - Assert.That(switcher.SentCommands, Has.Count.EqualTo(0)); - }); + Assert.ThrowsAsync(() => sut.Recorder.AddPause(12).WithTimeout()); + Assert.That(switcher.SentCommands, Has.Count.EqualTo(0)); } [Test] @@ -245,4 +205,43 @@ public async Task RecordMacro() Assert.That(command.Description, Is.EqualTo("My Description")); }); } + + [Test] + public async Task Continue() + { + var switcher = new AtemSwitcherFake(); + var sut = new MacroSystem(switcher); + sut.Populate(5); + + await sut.Player.Continue().WithTimeout(); + + Assert.That(switcher.SentCommands, Has.Count.EqualTo(1)); + var command = switcher.SentCommands[0].As(); + Assert.That(command.Action, Is.EqualTo(MacroAction.Continue)); + } + + [Test] + public async Task AddWaitForUser() + { + var switcher = new AtemSwitcherFake(); + var sut = new MacroSystem(switcher); + + sut.Populate(5); + sut.Recorder.UpdateCurrentlyRecording(sut[2]); + + await sut.Recorder.AddWaitForUser().WithTimeout(); + + Assert.That(switcher.SentCommands, Has.Count.EqualTo(1)); + var command = switcher.SentCommands[0].As(); + Assert.That(command.Action, Is.EqualTo(MacroAction.InsertUserWait)); + } + + [Test] + public void AddWaitForUser_WithoutRecording() + { + var switcher = new AtemSwitcherFake(); + var sut = new MacroSystem(switcher); + + Assert.ThrowsAsync(() => sut.Recorder.AddWaitForUser().WithTimeout()); + } } diff --git a/AtemSharp/Commands/Macro/MacroActionCommand.cs b/AtemSharp/Commands/Macro/MacroActionCommand.cs index aa628728..74767b04 100644 --- a/AtemSharp/Commands/Macro/MacroActionCommand.cs +++ b/AtemSharp/Commands/Macro/MacroActionCommand.cs @@ -1,25 +1,27 @@ -using System.Diagnostics.CodeAnalysis; - namespace AtemSharp.Commands.Macro; -// TODO #74: Capture test data and create tests for this command /// /// Used to run, stop, delete, etc. a macro /// [Command("MAct")] [BufferSize(4)] -public partial class MacroActionCommand(State.Macro.Macro macro, MacroAction action) : SerializedCommand +public partial class MacroActionCommand : SerializedCommand { - [SerializedField(0)] [InternalProperty] private readonly ushort _index = macro.Id; + [SerializedField(0)] [InternalProperty] private readonly ushort _index; - [SerializedField(2)] [InternalProperty] private readonly MacroAction _action = action; + [SerializedField(2)] [InternalProperty] private readonly MacroAction _action; - private void SerializeInternal(byte[] buffer) + private MacroActionCommand(ushort index, MacroAction action) { - if (_action is MacroAction.Stop or MacroAction.StopRecord or MacroAction.InsertUserWait or MacroAction.Continue) - { - buffer.WriteUInt16BigEndian(0xffff, 0); - } + _index = index; + _action = action; } + + public static MacroActionCommand Run(State.Macro.Macro macro) => new(macro.Id, MacroAction.Run); + public static MacroActionCommand Stop() => new(0xffff, MacroAction.Stop); + public static MacroActionCommand StopRecord() => new(0xffff, MacroAction.StopRecord); + public static MacroActionCommand InsertUserWait() => new(0xffff, MacroAction.InsertUserWait); + public static MacroActionCommand Continue() => new(0xffff, MacroAction.Continue); + public static MacroActionCommand Delete(State.Macro.Macro macro) => new(macro.Id, MacroAction.Delete); } diff --git a/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs b/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs index c03958b6..64fd4eb3 100644 --- a/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs +++ b/AtemSharp/Commands/Macro/MacroRunStatusUpdateCommand.cs @@ -33,7 +33,7 @@ public void Apply(IStateHolder switcher) var macroSystem = switcher.Macros; macroSystem.Player.UpdatePlayLooped(Loop); macroSystem.Player.UpdateCurrentlyPlaying(_isRunning ? macroSystem[_macroIndex] : null); - macroSystem.Player.UpdatePlaybackIsWaiting(IsWaiting); + macroSystem.Player.UpdatePlaybackIsWaitingForUserAction(IsWaiting); } [ExcludeFromCodeCoverage(Justification = "Obsolete")] diff --git a/AtemSharp/Extensions/VideoModeExtensions.cs b/AtemSharp/Extensions/VideoModeExtensions.cs index d5795d5b..f57c87e3 100644 --- a/AtemSharp/Extensions/VideoModeExtensions.cs +++ b/AtemSharp/Extensions/VideoModeExtensions.cs @@ -1,42 +1,44 @@ +using System.Diagnostics.CodeAnalysis; using AtemSharp.State.Settings; namespace AtemSharp.Extensions; public static class VideoModeExtensions { - public static ushort FramesPerSecond(this VideoMode videoMode) + private static readonly IReadOnlyDictionary FramesByVideoMode = new Dictionary { - return videoMode switch - { - VideoMode.N525i5994NTSC => 60, - VideoMode.P625i50PAL => 50, - VideoMode.N525i5994169 => 60, - VideoMode.P625i50169 => 50, - VideoMode.P720p50 => 50, - VideoMode.N720p5994 => 60, - VideoMode.P1080i50 => 50, - VideoMode.N1080i5994 => 60, - VideoMode.N1080p2398 => 24, - VideoMode.N1080p24 => 24, - VideoMode.P1080p25 => 25, - VideoMode.N1080p2997 => 30, - VideoMode.P1080p50 => 50, - VideoMode.N1080p5994 => 60, - VideoMode.N4KHDp2398 => 24, - VideoMode.N4KHDp24 => 24, - VideoMode.P4KHDp25 => 25, - VideoMode.N4KHDp2997 => 30, - VideoMode.P4KHDp5000 => 50, - VideoMode.N4KHDp5994 => 60, - VideoMode.N8KHDp2398 => 24, - VideoMode.N8KHDp24 => 24, - VideoMode.P8KHDp25 => 25, - VideoMode.N8KHDp2997 => 30, - VideoMode.P8KHDp50 => 50, - VideoMode.N8KHDp5994 => 60, - VideoMode.N1080p30 => 30, - VideoMode.N1080p60 => 60, - _ => throw new ArgumentOutOfRangeException() - }; - } + {VideoMode.N525i5994NTSC, 60}, + {VideoMode.P625i50PAL, 50}, + {VideoMode.N525i5994169, 60}, + {VideoMode.P625i50169, 50}, + {VideoMode.P720p50, 50}, + {VideoMode.N720p5994, 60}, + {VideoMode.P1080i50, 50}, + {VideoMode.N1080i5994, 60}, + {VideoMode.N1080p2398, 24}, + {VideoMode.N1080p24, 24}, + {VideoMode.P1080p25, 25}, + {VideoMode.N1080p2997, 30}, + {VideoMode.P1080p50, 50}, + {VideoMode.N1080p5994, 60}, + {VideoMode.N4KHDp2398, 24}, + {VideoMode.N4KHDp24, 24}, + {VideoMode.P4KHDp25, 25}, + {VideoMode.N4KHDp2997, 30}, + {VideoMode.P4KHDp5000, 50}, + {VideoMode.N4KHDp5994, 60}, + {VideoMode.N8KHDp2398, 24}, + {VideoMode.N8KHDp24, 24}, + {VideoMode.P8KHDp25, 25}, + {VideoMode.N8KHDp2997, 30}, + {VideoMode.P8KHDp50, 50}, + {VideoMode.N8KHDp5994, 60}, + {VideoMode.N1080p30, 30}, + {VideoMode.N1080p60, 60}, + }.AsReadOnly(); + + [ExcludeFromCodeCoverage(Justification = "The test would just repeat the code")] + public static ushort FramesPerSecond(this VideoMode videoMode) + => FramesByVideoMode[videoMode]; + } diff --git a/AtemSharp/State/Macro/Macro.cs b/AtemSharp/State/Macro/Macro.cs index ffab090e..aab69078 100644 --- a/AtemSharp/State/Macro/Macro.cs +++ b/AtemSharp/State/Macro/Macro.cs @@ -6,20 +6,45 @@ namespace AtemSharp.State.Macro; +/// +/// Represents a macro in the ATEM switcher +/// [ExcludeFromCodeCoverage(Justification="Auto-Properties aren't tested")] [DebuggerDisplay("{" + nameof(ToString) + ",nq}")] public partial class Macro(IAtemSwitcher switcher) { + /// + /// Gets the id of the macro + /// public ushort Id { get; internal set; } + /// + /// Gets/Sets the name + /// private string _name = string.Empty; + + /// + /// Gets/Sets the additional description + /// private string _description = string.Empty; + + /// + /// Gets whether this macro slot contains an actual macro + /// [ReadOnly(true)] private bool _isUsed; + + /// + /// Gets whether this macro uses unsupported operations + /// [ReadOnly(true)] private bool _hasUnsupportedOps; [ExcludeFromCodeCoverage] public override string ToString() => $"{GetType().Name} #{Id} ({Name})"; + /// + /// Runs this macro + /// + /// When the macro is not used public async Task Run() { if (!IsUsed) @@ -27,14 +52,27 @@ public async Task Run() throw new InvalidOperationException("Cannot run macros that are unused"); } - await switcher.SendCommandAsync(new MacroActionCommand(this, MacroAction.Run)); + await switcher.SendCommandAsync(MacroActionCommand.Run(this)); } + /// + /// Records a macro into this slot + /// + /// The name of the new macro + /// The description of the new macro public async Task Record(string name, string description) { await switcher.SendCommandAsync(new MacroRecordCommand(this) { Name = name, Description = description }); } + /// + /// Empties this macro slot + /// + public async Task Delete() + { + await switcher.SendCommandAsync(MacroActionCommand.Delete(this)); + } + private partial void SendNameUpdateCommand(string value) { switcher.SendCommandAsync(new MacroPropertiesCommand(this) { Name = value }).FireAndForget(); diff --git a/AtemSharp/State/Macro/MacroPlayer.cs b/AtemSharp/State/Macro/MacroPlayer.cs index dcaf734b..7abc4515 100644 --- a/AtemSharp/State/Macro/MacroPlayer.cs +++ b/AtemSharp/State/Macro/MacroPlayer.cs @@ -4,28 +4,44 @@ namespace AtemSharp.State.Macro; +/// +/// The playback engine of the macro system +/// public partial class MacroPlayer(IAtemSwitcher switcher) { + /// + /// Gets/sets whether the macro player should continuously repeat a playing macro until told to stop + /// private bool _playLooped; - [ReadOnly(true)] private bool _playbackIsWaiting; - [ReadOnly(true)] private Macro? _currentlyPlaying; + /// + /// Gets whether the currently playing macro waits for the user to tell it to continue + /// + [ReadOnly(true)] private bool _playbackIsWaitingForUserAction; + /// + /// Gets the currently playing macro or null if none is playing + /// + [ReadOnly(true)] private Macro? _currentlyPlaying; private partial void SendPlayLoopedUpdateCommand(bool value) { switcher.SendCommandAsync(new MacroRunStatusCommand { Loop = value }).FireAndForget(); } + /// + /// Stops playing the current macro + /// public async Task StopPlayback() { - var macroToStop = _currentlyPlaying; - - if (macroToStop is null) - { - return; - } + await switcher.SendCommandAsync(MacroActionCommand.Stop()); + } - await switcher.SendCommandAsync(new MacroActionCommand(macroToStop, MacroAction.Stop)); + /// + /// Tells the currently running macro to continue playing when it is waiting for the user to tell it so + /// + public async Task Continue() + { + await switcher.SendCommandAsync(MacroActionCommand.Continue()); } } diff --git a/AtemSharp/State/Macro/MacroRecorder.cs b/AtemSharp/State/Macro/MacroRecorder.cs index ccc3802b..1a5a13df 100644 --- a/AtemSharp/State/Macro/MacroRecorder.cs +++ b/AtemSharp/State/Macro/MacroRecorder.cs @@ -4,22 +4,29 @@ namespace AtemSharp.State.Macro; +/// +/// The recording part of the macro system +/// public partial class MacroRecorder(IAtemSwitcher switcher) { + /// + /// Gets the macro which is currently being recorded or null if no recording is in progress + /// [ReadOnly(true)] private Macro? _currentlyRecording; + /// + /// Stops recording a macro + /// public async Task StopRecording() { - var macroToStop = _currentlyRecording; - - if (macroToStop is null) - { - return; - } - - await switcher.SendCommandAsync(new MacroActionCommand(macroToStop, MacroAction.StopRecord)); + await switcher.SendCommandAsync(MacroActionCommand.StopRecord()); } + /// + /// Adds a timed pause to the macro + /// + /// The pause time in frames + /// When no macro is being recorded public async Task AddPause(ushort frameCount) { if (_currentlyRecording is null) @@ -30,8 +37,25 @@ public async Task AddPause(ushort frameCount) await switcher.SendCommandAsync(new MacroAddTimedPauseCommand { Frames = frameCount }); } + /// + /// Adds a timed pause to the macro according to the current video mode + /// + /// The timespan to wait. It will be converted to frames according to the currently set video mode public async Task AddPause(TimeSpan duration) { await AddPause((ushort)(duration.TotalSeconds * switcher.State.Settings.VideoMode.FramesPerSecond())); } + + /// + /// Adds a pause to the macro that prompts the user to tell it to continue + /// + public async Task AddWaitForUser() + { + if (_currentlyRecording is null) + { + throw new InvalidOperationException("Can only add pause while recording."); + } + + await switcher.SendCommandAsync(MacroActionCommand.InsertUserWait()); + } } diff --git a/stryker-config.json b/stryker-config.json index 08d0b11a..2cd831c9 100644 --- a/stryker-config.json +++ b/stryker-config.json @@ -1 +1 @@ -{ "stryker-config": { "project": "AtemSharp/AtemSharp.csproj", "test-projects": [ "AtemSharp.Tests/AtemSharp.Tests.csproj" ], "coverage-analysis": "perTestInIsolation", "mutate": [ "!**/*.g.cs" ], "ignore-methods": [ "Debug.*", "Log*" ], "configuration": "Release", "reporters": [ "progress", "html" ], "thresholds": { "high": 90, "low": 80, "break": 50 } }, "wrapper_config": { "pruning": { "max_number_to_keep": 10 } } } \ No newline at end of file +{ "stryker-config": { "project": "AtemSharp/AtemSharp.csproj", "test-projects": [ "AtemSharp.Tests/AtemSharp.Tests.csproj" ], "coverage-analysis": "perTestInIsolation", "mutate": [ "!**/*.g.cs" ], "ignore-methods": [ "Debug.*", "Log*", "InvalidOperationException.ctor" ], "configuration": "Release", "reporters": [ "progress", "html" ], "thresholds": { "high": 90, "low": 80, "break": 50 } }, "wrapper_config": { "pruning": { "max_number_to_keep": 10 } } } \ No newline at end of file From 5399144925f67fffca477caf32b283b1c92d5ef8 Mon Sep 17 00:00:00 2001 From: Markus Palcer Date: Mon, 29 Dec 2025 17:01:41 +0100 Subject: [PATCH 2/2] add tests --- .../State/BoundsConvenienceProperty.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 AtemSharp.Tests/State/BoundsConvenienceProperty.cs diff --git a/AtemSharp.Tests/State/BoundsConvenienceProperty.cs b/AtemSharp.Tests/State/BoundsConvenienceProperty.cs new file mode 100644 index 00000000..d7fc6141 --- /dev/null +++ b/AtemSharp.Tests/State/BoundsConvenienceProperty.cs @@ -0,0 +1,38 @@ +using System.Drawing; +using AtemSharp.State.Video.MixEffect.UpstreamKeyer; + +namespace AtemSharp.Tests.State; + +[TestFixture] +public class BoundsConvenienceProperty +{ + [Test] + public void UpstreamKeyerDigitalVideoEffectSettings() + { + var sut = new UpstreamKeyerDigitalVideoEffectSettings + { + Location = new PointF(12.3f, 4.56f), + Size = new SizeF(6f, 12f), + }; + Assert.Multiple(() => + { + Assert.That(sut.Bounds.Location, Is.EqualTo(new PointF(12.3f, 4.56f))); + Assert.That(sut.Bounds.Size, Is.EqualTo(new SizeF(6f, 12f))); + }); + } + + [Test] + public void UpstreamKeyerFlyKeyframe() + { + var sut = new UpstreamKeyerFlyKeyframe + { + Location = new PointF(12.3f, 4.56f), + Size = new SizeF(6f, 12f), + }; + Assert.Multiple(() => + { + Assert.That(sut.Bounds.Location, Is.EqualTo(new PointF(12.3f, 4.56f))); + Assert.That(sut.Bounds.Size, Is.EqualTo(new SizeF(6f, 12f))); + }); + } +}