diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5de0996
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+/build*
+docs/build*
+/xml
+docs/xml
+out
+mpich
+cmake-build-*
+.idea/
+.vs/
+.vscode/
+scripts/variants*.csv
+scripts/variants*.xlsx
+*venv*
+sln/
+CMakeSettings.json
+.DS_Store
+.cache
+install
+*.pyc
diff --git a/BugPro/BugPro.csproj b/BugPro/BugPro.csproj
new file mode 100644
index 0000000..378dff9
--- /dev/null
+++ b/BugPro/BugPro.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/BugPro/Program.cs b/BugPro/Program.cs
new file mode 100644
index 0000000..9f37d8c
--- /dev/null
+++ b/BugPro/Program.cs
@@ -0,0 +1,193 @@
+using System;
+using Stateless;
+
+namespace BugWorkflow
+{
+ public sealed class Bug
+ {
+ public enum BugState
+ {
+ New,
+ Triage,
+ WaitingInfo,
+ Fixing,
+ WaitFixApprove,
+ Returned,
+ Reopened,
+ Closed
+ }
+
+ public enum BugTrigger
+ {
+ StartTriage,
+ RequestMoreInfo,
+ BackToTriage,
+ StartFix,
+ FinisFix,
+ MarkNotABug,
+ MarkWontFix,
+ MarkDuplicate,
+ MarkCannotReproduce,
+ ResolveFix,
+ RejectFix,
+ Reopen
+ }
+
+ private readonly StateMachine _machine;
+
+ public BugState State => _machine.State;
+
+ public Bug()
+ {
+ _machine = new StateMachine(BugState.New);
+ ConfigureStateMachine();
+ }
+
+ private void ConfigureStateMachine()
+ {
+ _machine.OnUnhandledTrigger((state, trigger) =>
+ {
+ throw new InvalidOperationException(
+ $"триггер '{trigger}' недопустим в состоянии '{state}'.");
+ });
+
+ _machine.Configure(BugState.New)
+ .Permit(BugTrigger.StartTriage, BugState.Triage);
+
+ _machine.Configure(BugState.Triage)
+ .Permit(BugTrigger.RequestMoreInfo, BugState.WaitingInfo)
+ .Permit(BugTrigger.StartFix, BugState.Fixing)
+ .Permit(BugTrigger.MarkNotABug, BugState.Closed)
+ .Permit(BugTrigger.MarkWontFix, BugState.Closed)
+ .Permit(BugTrigger.MarkDuplicate, BugState.Closed);
+
+ _machine.Configure(BugState.WaitingInfo)
+ .Permit(BugTrigger.BackToTriage, BugState.Triage)
+ .Permit(BugTrigger.StartFix, BugState.Fixing);
+
+ _machine.Configure(BugState.Fixing)
+ .Permit(BugTrigger.FinisFix, BugState.WaitFixApprove)
+ .Permit(BugTrigger.RequestMoreInfo, BugState.WaitingInfo)
+ .Permit(BugTrigger.MarkCannotReproduce, BugState.Returned);
+
+ _machine.Configure(BugState.WaitFixApprove)
+ .Permit(BugTrigger.ResolveFix, BugState.Closed)
+ .Permit(BugTrigger.RejectFix, BugState.Returned);
+
+ _machine.Configure(BugState.Reopened)
+ .Permit(BugTrigger.BackToTriage, BugState.Triage);
+
+ _machine.Configure(BugState.Closed)
+ .Permit(BugTrigger.Reopen, BugState.Reopened);
+ }
+
+ public void StartTriage()
+ {
+ _machine.Fire(BugTrigger.StartTriage);
+ }
+
+ public void RequestMoreInfo()
+ {
+ _machine.Fire(BugTrigger.RequestMoreInfo);
+ }
+
+ public void BackToTriage()
+ {
+ _machine.Fire(BugTrigger.BackToTriage);
+ }
+
+ public void StartFix()
+ {
+ _machine.Fire(BugTrigger.StartFix);
+ }
+
+ public void FinishFix()
+ {
+ _machine.Fire(BugTrigger.FinisFix);
+ }
+
+ public void MarkNotABug()
+ {
+ _machine.Fire(BugTrigger.MarkNotABug);
+ }
+
+ public void MarkWontFix()
+ {
+ _machine.Fire(BugTrigger.MarkWontFix);
+ }
+
+ public void MarkDuplicate()
+ {
+ _machine.Fire(BugTrigger.MarkDuplicate);
+ }
+
+ public void MarkCannotReproduce()
+ {
+ _machine.Fire(BugTrigger.MarkCannotReproduce);
+ }
+
+ public void ResolveFix()
+ {
+ _machine.Fire(BugTrigger.ResolveFix);
+ }
+
+ public void RejectFix()
+ {
+ _machine.Fire(BugTrigger.RejectFix);
+ }
+
+ public void Reopen()
+ {
+ _machine.Fire(BugTrigger.Reopen);
+ }
+
+ public void CheckFixResult(bool solved)
+ {
+ if (solved)
+ ResolveFix();
+ else
+ RejectFix();
+ }
+
+ public override string ToString()
+ {
+ return $"State = {State}";
+ }
+ }
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var bug = new Bug();
+ Console.WriteLine("начальное состояние: " + bug);
+
+ bug.StartTriage();
+ Console.WriteLine("после StartTriage: " + bug);
+
+ bug.StartFix();
+ Console.WriteLine("после StartFix: " + bug);
+
+ bug.FinishFix();
+ Console.WriteLine("после FinishFix: " + bug);
+
+ bug.CheckFixResult(solved: true);
+ Console.WriteLine("после CheckFixResult(true): " + bug);
+
+ bug.Reopen();
+ Console.WriteLine("после Reopen: " + bug);
+
+ bug.BackToTriage();
+ Console.WriteLine("после BackToTriage: " + bug);
+
+ try
+ {
+ bug.ResolveFix();
+ }
+ catch (InvalidOperationException ex)
+ {
+ Console.WriteLine("Ожидаемая ошибка: " + ex.Message);
+ }
+ }
+ }
+}
diff --git a/BugTests/BugTests.csproj b/BugTests/BugTests.csproj
new file mode 100644
index 0000000..2ad20b5
--- /dev/null
+++ b/BugTests/BugTests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BugTests/UnitTest1.cs b/BugTests/UnitTest1.cs
new file mode 100644
index 0000000..f1d48f3
--- /dev/null
+++ b/BugTests/UnitTest1.cs
@@ -0,0 +1,250 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using BugWorkflow;
+
+namespace BugWorkflow.Tests
+{
+ [TestClass]
+ public class BugTests
+ {
+ [TestMethod]
+ public void NewBug_ShouldHaveInitialStateNew()
+ {
+ var bug = new Bug();
+
+ Assert.AreEqual(Bug.BugState.New, bug.State);
+ }
+
+ [TestMethod]
+ public void RequestMoreInfo_FromTriage_ShouldMoveToWaitingInfo()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+
+ bug.RequestMoreInfo();
+
+ Assert.AreEqual(Bug.BugState.WaitingInfo, bug.State);
+ }
+
+ [TestMethod]
+ public void StartTriage_FromNew_ShouldMoveToTriage()
+ {
+ var bug = new Bug();
+
+ bug.StartTriage();
+
+ Assert.AreEqual(Bug.BugState.Triage, bug.State);
+ }
+
+ [TestMethod]
+ public void BackToTriage_FromWaitingInfo_ShouldMoveToTriage()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.RequestMoreInfo();
+
+ bug.BackToTriage();
+
+ Assert.AreEqual(Bug.BugState.Triage, bug.State);
+ }
+
+ [TestMethod]
+ public void StartFix_FromTriage_ShouldMoveToFixing()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+
+ bug.StartFix();
+
+ Assert.AreEqual(Bug.BugState.Fixing, bug.State);
+ }
+
+ [TestMethod]
+ public void FinishFix_FromFixing_ShouldMoveToWaitFixApprove()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+
+ bug.FinishFix();
+
+ Assert.AreEqual(Bug.BugState.WaitFixApprove, bug.State);
+ }
+
+ [TestMethod]
+ public void StartFix_FromWaitingInfo_ShouldMoveToFixing()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.RequestMoreInfo();
+
+ bug.StartFix();
+
+ Assert.AreEqual(Bug.BugState.Fixing, bug.State);
+ }
+
+ [TestMethod]
+ public void ResolveFix_FromWaitFixApprove_ShouldCloseBug()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+ bug.FinishFix();
+
+ bug.ResolveFix();
+
+ Assert.AreEqual(Bug.BugState.Closed, bug.State);
+ }
+
+ [TestMethod]
+ public void RejectFix_FromWaitFixApprove_ShouldMoveToReturned()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+ bug.FinishFix();
+
+ bug.RejectFix();
+
+ Assert.AreEqual(Bug.BugState.Returned, bug.State);
+ }
+
+ [TestMethod]
+ public void CheckFixResult_False_FromWaitFixApprove_ShouldReturnBug()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+ bug.FinishFix();
+
+ bug.CheckFixResult(false);
+
+ Assert.AreEqual(Bug.BugState.Returned, bug.State);
+ }
+
+ [TestMethod]
+ public void CheckFixResult_True_FromWaitFixApprove_ShouldCloseBug()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+ bug.FinishFix();
+
+ bug.CheckFixResult(true);
+
+ Assert.AreEqual(Bug.BugState.Closed, bug.State);
+ }
+
+ [TestMethod]
+ public void MarkNotABug_FromTriage_ShouldCloseBug()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+
+ bug.MarkNotABug();
+
+ Assert.AreEqual(Bug.BugState.Closed, bug.State);
+ }
+
+ [TestMethod]
+ public void MarkDuplicate_FromTriage_ShouldCloseBug()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+
+ bug.MarkDuplicate();
+
+ Assert.AreEqual(Bug.BugState.Closed, bug.State);
+ }
+
+ [TestMethod]
+ public void MarkWontFix_FromTriage_ShouldCloseBug()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+
+ bug.MarkWontFix();
+
+ Assert.AreEqual(Bug.BugState.Closed, bug.State);
+ }
+
+ [TestMethod]
+ public void MarkCannotReproduce_FromFixing_ShouldMoveToReturned()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+
+ bug.MarkCannotReproduce();
+
+ Assert.AreEqual(Bug.BugState.Returned, bug.State);
+ }
+
+ [TestMethod]
+ public void Reopen_FromClosed_ShouldMoveToReopened()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.MarkNotABug();
+
+ bug.Reopen();
+
+ Assert.AreEqual(Bug.BugState.Reopened, bug.State);
+ }
+
+ [TestMethod]
+ public void EnteringTriage_AfterReopen_ShouldMoveToTriage()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.MarkWontFix();
+ bug.Reopen();
+
+ bug.BackToTriage();
+
+ Assert.AreEqual(Bug.BugState.Triage, bug.State);
+ }
+
+ [TestMethod]
+ public void BackToTriage_FromReopened_ShouldMoveToTriage()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.MarkDuplicate();
+ bug.Reopen();
+
+ bug.BackToTriage();
+
+ Assert.AreEqual(Bug.BugState.Triage, bug.State);
+ }
+
+ [TestMethod]
+ public void ResolveFix_FromFixingWithoutFinishFix_ShouldThrowInvalidOperationException()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+
+ Assert.ThrowsException(() => bug.ResolveFix());
+ }
+
+ [TestMethod]
+ public void StartFix_FromNew_ShouldThrowInvalidOperationException()
+ {
+ var bug = new Bug();
+
+ Assert.ThrowsException(() => bug.StartFix());
+ }
+
+ [TestMethod]
+ public void Reopen_FromReturned_ShouldThrowInvalidOperationException()
+ {
+ var bug = new Bug();
+ bug.StartTriage();
+ bug.StartFix();
+ bug.MarkCannotReproduce();
+
+ Assert.ThrowsException(() => bug.Reopen());
+ }
+ }
+}
diff --git a/ST-4.sln b/ST-4.sln
index 58ea566..3bd9349 100644
--- a/ST-4.sln
+++ b/ST-4.sln
@@ -3,6 +3,10 @@ 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", "{B02934F6-7862-449D-B910-D45427D6BAF1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugTests", "BugTests\BugTests.csproj", "{CCB4C1B0-0EF2-43FE-9125-5C3C130E3744}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -11,4 +15,14 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B02934F6-7862-449D-B910-D45427D6BAF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B02934F6-7862-449D-B910-D45427D6BAF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B02934F6-7862-449D-B910-D45427D6BAF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B02934F6-7862-449D-B910-D45427D6BAF1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CCB4C1B0-0EF2-43FE-9125-5C3C130E3744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CCB4C1B0-0EF2-43FE-9125-5C3C130E3744}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CCB4C1B0-0EF2-43FE-9125-5C3C130E3744}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CCB4C1B0-0EF2-43FE-9125-5C3C130E3744}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
EndGlobal
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..230afd4
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "9.0.312",
+ "rollForward": "latestFeature"
+ }
+}