/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System.Collections.Generic; using Generators; using GeneratorModel = Opc.Ua.Generators; using NUnit.Framework; namespace Opc.Ua.OpenUsd.Tests.Generator { /// /// Holds the six generator-set methods to what they claim to do. /// /// /// The method nodes themselves are plumbing; is /// the behaviour, and it is where a wrong answer matters. A method that silently /// does nothing is the worst outcome of the three - worse than one that refuses, /// because a client cannot tell it apart from success. /// [TestFixture] [Category("Generators")] public sealed class GeneratorMethodTests { /// /// Start takes a shut-down set through Ready and into cranking. /// /// /// Two declared transitions, not one: a set becomes available before it /// cranks, and a client watching the state machine must see both. /// [Test] public void StartFromOffPassesThroughReady() { GeneratorSimulation simulation = SimulationHarness.Create(); List seen = SimulationHarness.RecordStates(simulation); Assert.Multiple(() => { Assert.That(GeneratorCommands.Start(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Starting)); Assert.That(seen, Is.EqualTo(new[] { GeneratorRunState.Ready, GeneratorRunState.Starting, })); }); } /// /// Start from Ready cranks directly. /// [Test] public void StartFromReadyCranksDirectly() { GeneratorSimulation simulation = SimulationHarness.Create(); simulation.RequestState(GeneratorRunState.Ready); Assert.Multiple(() => { Assert.That(GeneratorCommands.Start(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Starting)); }); } /// /// Starting a set that is already running is refused rather than ignored. /// [Test] public void StartOnALoadedSetIsRefused() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); Assert.Multiple(() => { Assert.That(GeneratorCommands.Start(simulation), Is.False); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Loaded)); }); } /// /// Stop takes a loaded set into cooldown rather than straight to rest. /// [Test] public void StopGoesThroughCooldown() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); Assert.Multiple(() => { Assert.That(GeneratorCommands.Stop(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Cooldown)); }); } /// /// Stopping a set that is already off is refused. /// [Test] public void StopOnAStoppedSetIsRefused() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.Multiple(() => { Assert.That(GeneratorCommands.Stop(simulation), Is.False); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Off)); }); } /// /// The emergency stop drops a loaded set immediately. /// [Test] public void EmergencyStopDropsALoadedSet() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); Assert.Multiple(() => { Assert.That(GeneratorCommands.EmergencyStop(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.EmergencyStopped)); Assert.That(simulation.IsLoaded, Is.False); Assert.That(simulation.ProtectionTripped, Is.True); }); } /// /// The emergency stop is declared only out of the states in which the engine /// is turning at speed. /// /// /// This pins the specification's shape rather than endorsing it. A real /// panel stops from anywhere; GeneratorStateMachineType declares /// RunningToEmergencyStopped, LoadedToEmergencyStopped and /// ParalleledToEmergencyStopped and nothing else. If a later revision /// of the model adds more, this test is what says so. /// [Test] public void EmergencyStopFollowsTheDeclaredTransitions() { (GeneratorRunState From, bool Declared)[] cases = [ (GeneratorRunState.Running, true), (GeneratorRunState.Loaded, true), (GeneratorRunState.Paralleled, true), (GeneratorRunState.Off, false), (GeneratorRunState.Starting, false), (GeneratorRunState.Warmup, false), ]; Assert.Multiple(() => { foreach ((GeneratorRunState from, bool declared) in cases) { Assert.That( GeneratorSimulation.IsLegalTransition( from, GeneratorRunState.EmergencyStopped), Is.EqualTo(declared), $"{from}->EmergencyStopped"); } }); } /// /// ResetFaults returns a tripped set to Off. /// [Test] public void ResetFaultsClearsALatchedTrip() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); simulation.Trip(); Assert.Multiple(() => { Assert.That(GeneratorCommands.ResetFaults(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Off)); Assert.That(simulation.ProtectionTripped, Is.False); }); } /// /// ResetFaults clears an emergency stop as well as a protection trip. /// [Test] public void ResetFaultsClearsAnEmergencyStop() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); GeneratorCommands.EmergencyStop(simulation); Assert.Multiple(() => { Assert.That(GeneratorCommands.ResetFaults(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Off)); }); } /// /// Resetting a healthy set succeeds without moving it. /// /// /// Answering Bad here would only train an operator to ignore the result of /// pressing reset. /// [Test] public void ResetFaultsOnAHealthySetIsANoOp() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); Assert.Multiple(() => { Assert.That(GeneratorCommands.ResetFaults(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Loaded)); }); } /// /// A reset set can be started again. /// /// /// Without this, a trip is a one-way door and the sample can only ever show /// a plant winding down. /// [Test] public void ASetCanRunAgainAfterAResetTrip() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.That( SimulationHarness.AdvanceUntil(simulation, GeneratorRunState.Loaded), Is.True); simulation.Trip(); GeneratorCommands.ResetFaults(simulation); Assert.Multiple(() => { Assert.That(GeneratorCommands.Start(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Starting)); }); } /// /// StartTest starts a shut-down set the same way Start does. /// [Test] public void StartTestStartsAStoppedSet() { GeneratorSimulation simulation = SimulationHarness.Create(); Assert.Multiple(() => { Assert.That(GeneratorCommands.StartTest(simulation), Is.True); Assert.That(simulation.State, Is.EqualTo(GeneratorRunState.Starting)); }); } /// /// A selector position naming a real mode is accepted. /// [TestCase((int)GeneratorModel.GeneratorOperatingModeEnum.Auto)] [TestCase((int)GeneratorModel.GeneratorOperatingModeEnum.Manual)] [TestCase((int)GeneratorModel.GeneratorOperatingModeEnum.Test)] public void ADeclaredOperatingModeIsAccepted(int requested) { ArrayOf arguments = new Variant[] { new(requested) }; Assert.Multiple(() => { Assert.That( GeneratorCommands.TryReadOperatingMode( arguments, out GeneratorModel.GeneratorOperatingModeEnum mode), Is.True); Assert.That((int)mode, Is.EqualTo(requested)); }); } /// /// A selector position naming no mode is refused rather than stored. /// [Test] public void AnUndeclaredOperatingModeIsRefused() { ArrayOf arguments = new Variant[] { new(9999) }; Assert.That( GeneratorCommands.TryReadOperatingMode( arguments, out GeneratorModel.GeneratorOperatingModeEnum _), Is.False); } /// /// A call with no arguments is refused rather than throwing. /// [Test] public void AnEmptyArgumentListIsRefused() { Assert.That( GeneratorCommands.TryReadOperatingMode( default, out GeneratorModel.GeneratorOperatingModeEnum _), Is.False); } /// /// Commanding one set leaves the others where they were. /// [Test] public void CommandsActOnOneSetOnly() { GeneratorSimulation first = SimulationHarness.Create(0); GeneratorSimulation second = SimulationHarness.Create(1); Assert.That(SimulationHarness.AdvanceUntil(first, GeneratorRunState.Loaded), Is.True); Assert.That(SimulationHarness.AdvanceUntil(second, GeneratorRunState.Loaded), Is.True); GeneratorCommands.EmergencyStop(first); Assert.Multiple(() => { Assert.That(first.State, Is.EqualTo(GeneratorRunState.EmergencyStopped)); Assert.That(second.State, Is.EqualTo(GeneratorRunState.Loaded)); }); } } }