diff --git a/BugPro/BugPro.csproj b/BugPro/BugPro.csproj new file mode 100644 index 0000000..92333c8 --- /dev/null +++ b/BugPro/BugPro.csproj @@ -0,0 +1,15 @@ + + + Exe + net9.0 + BugPro + BugPro + enable + enable + 13 + false + + + + + diff --git a/BugPro/Program.cs b/BugPro/Program.cs new file mode 100644 index 0000000..19550c7 --- /dev/null +++ b/BugPro/Program.cs @@ -0,0 +1,152 @@ +using Stateless; + +namespace BugPro; + +public enum IssuePhase +{ + New, + Analysis, + InProgress, + NeedMoreInfo, + NotReproducible, + Duplicate, + Fixed, + Closed, + Reopened, + Deferred +} + +public enum TransitionSignal +{ + StartAnalysis, + RequestInfo, + MarkDuplicate, + MarkNotReproducible, + StartFix, + Fix, + ConfirmResolved, + RejectFix, + Reopen, + Defer, + ReturnToAnalysis +} + +public sealed class Bug +{ + private IssuePhase phase; + private readonly StateMachine engine; + + public Bug() + { + phase = IssuePhase.New; + engine = new StateMachine( + () => phase, + s => phase = s); + + BuildWorkflow(); + } + + public IssuePhase State => phase; + + public void StartAnalysis() => Apply(TransitionSignal.StartAnalysis); + + public void RequestInfo() => Apply(TransitionSignal.RequestInfo); + + public void MarkDuplicate() => Apply(TransitionSignal.MarkDuplicate); + + public void MarkNotReproducible() => Apply(TransitionSignal.MarkNotReproducible); + + public void StartFix() => Apply(TransitionSignal.StartFix); + + public void Fix() => Apply(TransitionSignal.Fix); + + public void ConfirmResolved() => Apply(TransitionSignal.ConfirmResolved); + + public void RejectFix() => Apply(TransitionSignal.RejectFix); + + public void Reopen() => Apply(TransitionSignal.Reopen); + + public void Defer() => Apply(TransitionSignal.Defer); + + public void ReturnToAnalysis() => Apply(TransitionSignal.ReturnToAnalysis); + + public bool CanStartAnalysis() => engine.CanFire(TransitionSignal.StartAnalysis); + + public bool CanFix() => engine.CanFire(TransitionSignal.Fix); + + public bool CanClose() => engine.CanFire(TransitionSignal.ConfirmResolved); + + public bool CanReopen() => engine.CanFire(TransitionSignal.Reopen); + + public bool CanReturnToAnalysis() => engine.CanFire(TransitionSignal.ReturnToAnalysis); + + private void Apply(TransitionSignal signal) => engine.Fire(signal); + + private void BuildWorkflow() + { + engine.Configure(IssuePhase.New) + .Permit(TransitionSignal.StartAnalysis, IssuePhase.Analysis) + .Permit(TransitionSignal.Defer, IssuePhase.Deferred); + + engine.Configure(IssuePhase.Analysis) + .Permit(TransitionSignal.RequestInfo, IssuePhase.NeedMoreInfo) + .Permit(TransitionSignal.MarkDuplicate, IssuePhase.Duplicate) + .Permit(TransitionSignal.MarkNotReproducible, IssuePhase.NotReproducible) + .Permit(TransitionSignal.StartFix, IssuePhase.InProgress) + .Permit(TransitionSignal.Defer, IssuePhase.Deferred); + + engine.Configure(IssuePhase.NeedMoreInfo) + .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis) + .Permit(TransitionSignal.Defer, IssuePhase.Deferred); + + engine.Configure(IssuePhase.InProgress) + .Permit(TransitionSignal.Fix, IssuePhase.Fixed) + .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis) + .Permit(TransitionSignal.Defer, IssuePhase.Deferred); + + engine.Configure(IssuePhase.Fixed) + .Permit(TransitionSignal.ConfirmResolved, IssuePhase.Closed) + .Permit(TransitionSignal.RejectFix, IssuePhase.Reopened); + + engine.Configure(IssuePhase.Closed) + .Permit(TransitionSignal.Reopen, IssuePhase.Reopened); + + engine.Configure(IssuePhase.Reopened) + .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis) + .Permit(TransitionSignal.StartFix, IssuePhase.InProgress) + .Permit(TransitionSignal.Defer, IssuePhase.Deferred); + + engine.Configure(IssuePhase.Deferred) + .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis); + + engine.Configure(IssuePhase.Duplicate) + .Permit(TransitionSignal.ConfirmResolved, IssuePhase.Closed); + + engine.Configure(IssuePhase.NotReproducible) + .Permit(TransitionSignal.ConfirmResolved, IssuePhase.Closed); + } +} + +internal static class Program +{ + private static void Main() + { + var ticket = new Bug(); + + PrintPhase(ticket, "регистрация"); + ticket.Defer(); + PrintPhase(ticket, "отложен"); + ticket.ReturnToAnalysis(); + ticket.StartAnalysis(); + PrintPhase(ticket, "разбор"); + ticket.StartFix(); + ticket.Fix(); + ticket.ConfirmResolved(); + PrintPhase(ticket, "завершён"); + } + + private static void PrintPhase(Bug ticket, string step) + { + Console.WriteLine($"[{step}] фаза: {ticket.State}"); + } +} diff --git a/BugTests/BugTests.csproj b/BugTests/BugTests.csproj new file mode 100644 index 0000000..7e8017c --- /dev/null +++ b/BugTests/BugTests.csproj @@ -0,0 +1,21 @@ + + + net9.0 + BugTests + BugTests + enable + enable + 13 + false + true + false + + + + + + + + + + diff --git a/BugTests/UnitTest1.cs b/BugTests/UnitTest1.cs new file mode 100644 index 0000000..ea0b56f --- /dev/null +++ b/BugTests/UnitTest1.cs @@ -0,0 +1,240 @@ +using BugPro; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BugTests; + +[TestClass] +public sealed class IssueWorkflowTests +{ + private Bug _ticket = null!; + + [TestInitialize] + public void Init() => _ticket = new Bug(); + + [TestCleanup] + public void Cleanup() => _ticket = null!; + + [TestMethod] + public void FreshTicket_StartsInNewPhase() + { + ExpectPhase(IssuePhase.New); + } + + [TestMethod] + public void NewTicket_OnlyStartAnalysisAllowed() + { + Assert.IsTrue(_ticket.CanStartAnalysis()); + Assert.IsFalse(_ticket.CanFix()); + Assert.IsFalse(_ticket.CanClose()); + Assert.IsFalse(_ticket.CanReopen()); + Assert.IsFalse(_ticket.CanReturnToAnalysis()); + } + + [TestMethod] + public void BeginReview_MovesToAnalysisPhase() + { + _ticket.StartAnalysis(); + ExpectPhase(IssuePhase.Analysis); + } + + [TestMethod] + public void RequestDetails_FromAnalysis_GoesToNeedMoreInfo() + { + GoToAnalysis(); + _ticket.RequestInfo(); + ExpectPhase(IssuePhase.NeedMoreInfo); + } + + [TestMethod] + public void ReturnFromDetails_RestoresAnalysis() + { + GoToNeedMoreInfo(); + _ticket.ReturnToAnalysis(); + ExpectPhase(IssuePhase.Analysis); + } + + [TestMethod] + public void LaunchFix_FromAnalysis_EntersInProgress() + { + GoToAnalysis(); + _ticket.StartFix(); + ExpectPhase(IssuePhase.InProgress); + } + + [TestMethod] + public void InProgress_AllowsFixAction() + { + GoToInProgress(); + Assert.IsTrue(_ticket.CanFix()); + Assert.IsFalse(_ticket.CanStartAnalysis()); + } + + [TestMethod] + public void ApplyFix_ReachesResolvedPhase() + { + GoToInProgress(); + _ticket.Fix(); + ExpectPhase(IssuePhase.Fixed); + } + + [TestMethod] + public void CloseTicket_CompletesHappyPath() + { + GoToInProgress(); + _ticket.Fix(); + _ticket.ConfirmResolved(); + ExpectPhase(IssuePhase.Closed); + } + + [TestMethod] + public void Reopen_FromClosed_SetsReopenedPhase() + { + GoToClosed(); + _ticket.Reopen(); + ExpectPhase(IssuePhase.Reopened); + } + + [TestMethod] + public void RejectResolution_FromFixed_OpensReopened() + { + GoToFixed(); + _ticket.RejectFix(); + ExpectPhase(IssuePhase.Reopened); + } + + [TestMethod] + public void Reopened_ResumesFixWork() + { + GoToReopened(); + _ticket.StartFix(); + ExpectPhase(IssuePhase.InProgress); + } + + [TestMethod] + public void MarkAsDuplicate_AndClose() + { + GoToAnalysis(); + _ticket.MarkDuplicate(); + _ticket.ConfirmResolved(); + ExpectPhase(IssuePhase.Closed); + } + + [TestMethod] + public void MarkAsNonReproducible_AndClose() + { + GoToAnalysis(); + _ticket.MarkNotReproducible(); + _ticket.ConfirmResolved(); + ExpectPhase(IssuePhase.Closed); + } + + [TestMethod] + public void Postpone_FromNew_GoesDeferred() + { + _ticket.Defer(); + ExpectPhase(IssuePhase.Deferred); + } + + [TestMethod] + public void Deferred_ReturnsToAnalysis() + { + _ticket.Defer(); + _ticket.ReturnToAnalysis(); + ExpectPhase(IssuePhase.Analysis); + } + + [TestMethod] + public void Fix_FromNew_Throws() + { + AssertBlocked(() => _ticket.Fix()); + } + + [TestMethod] + public void Close_FromAnalysis_Throws() + { + GoToAnalysis(); + AssertBlocked(() => _ticket.ConfirmResolved()); + } + + [TestMethod] + public void Reopen_FromFixed_Throws() + { + GoToFixed(); + AssertBlocked(() => _ticket.Reopen()); + } + + [TestMethod] + public void StartAnalysis_FromClosed_Throws() + { + GoToClosed(); + AssertBlocked(() => _ticket.StartAnalysis()); + } + + [TestMethod] + public void ReturnToAnalysis_FromNew_Throws() + { + AssertBlocked(() => _ticket.ReturnToAnalysis()); + } + + [TestMethod] + public void Defer_FromClosed_Throws() + { + GoToClosed(); + AssertBlocked(() => _ticket.Defer()); + } + + [TestMethod] + public void MarkDuplicate_FromNew_Throws() + { + AssertBlocked(() => _ticket.MarkDuplicate()); + } + + [TestMethod] + public void RequestInfo_FromInProgress_Throws() + { + GoToInProgress(); + AssertBlocked(() => _ticket.RequestInfo()); + } + + private void ExpectPhase(IssuePhase expected) + { + Assert.AreEqual(expected, _ticket.State); + } + + private static void AssertBlocked(Action action) + { + Assert.ThrowsException(action); + } + + private void GoToAnalysis() => _ticket.StartAnalysis(); + + private void GoToNeedMoreInfo() + { + GoToAnalysis(); + _ticket.RequestInfo(); + } + + private void GoToInProgress() + { + GoToAnalysis(); + _ticket.StartFix(); + } + + private void GoToFixed() + { + GoToInProgress(); + _ticket.Fix(); + } + + private void GoToClosed() + { + GoToFixed(); + _ticket.ConfirmResolved(); + } + + private void GoToReopened() + { + GoToFixed(); + _ticket.RejectFix(); + } +} diff --git a/ST-4.sln b/ST-4.sln index 58ea566..a4943af 100644 --- a/ST-4.sln +++ b/ST-4.sln @@ -1,14 +1,27 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugPro", "BugPro\BugPro.csproj", "{B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugTests", "BugTests\BugTests.csproj", "{262BB970-40D8-4B89-A423-615CE07FC740}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Release|Any CPU.Build.0 = Release|Any CPU + {262BB970-40D8-4B89-A423-615CE07FC740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {262BB970-40D8-4B89-A423-615CE07FC740}.Debug|Any CPU.Build.0 = Debug|Any CPU + {262BB970-40D8-4B89-A423-615CE07FC740}.Release|Any CPU.ActiveCfg = Release|Any CPU + {262BB970-40D8-4B89-A423-615CE07FC740}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection -EndGlobal +EndGlobal \ No newline at end of file