Skip to content
This repository was archived by the owner on Jun 10, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions BugPro/BugPro.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Stateless" Version="5.13.0" />
</ItemGroup>

</Project>
116 changes: 116 additions & 0 deletions BugPro/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using Stateless;

namespace BugPro
{
public enum State
{
Open,
InAnalysis,
InProgress,
Resolved,
Closed,
Reopened,
Rejected,
Deferred
}

public enum Trigger
{
Analyze,
Assign,
Resolve,
Confirm,
Reanalyze,
Reject,
Defer,
Resume,
Reopen
}

public class Bug
{
public const int MaxReopen = 3;

private readonly StateMachine<State, Trigger> _machine;
private int _reopenCount;

public Bug()
{
_machine = new StateMachine<State, Trigger>(State.Open);

_machine.Configure(State.Open)
.Permit(Trigger.Analyze, State.InAnalysis);

_machine.Configure(State.InAnalysis)
.Permit(Trigger.Assign, State.InProgress)
.Permit(Trigger.Defer, State.Deferred)
.Permit(Trigger.Reject, State.Rejected);

_machine.Configure(State.InProgress)
.Permit(Trigger.Resolve, State.Resolved)
.Permit(Trigger.Reanalyze, State.InAnalysis);

_machine.Configure(State.Resolved)
.Permit(Trigger.Confirm, State.Closed)
.Permit(Trigger.Reanalyze, State.InAnalysis);

_machine.Configure(State.Deferred)
.Permit(Trigger.Resume, State.InAnalysis);

_machine.Configure(State.Closed)
.PermitIf(Trigger.Reopen, State.Reopened, () => _reopenCount < MaxReopen);

_machine.Configure(State.Rejected)
.PermitIf(Trigger.Reopen, State.Reopened, () => _reopenCount < MaxReopen);

_machine.Configure(State.Reopened)
.OnEntry(() => _reopenCount++)
.Permit(Trigger.Analyze, State.InAnalysis);
}

public State CurrentState => _machine.State;

public int ReopenCount => _reopenCount;

public bool CanFire(Trigger trigger) => _machine.CanFire(trigger);

public void Analyze() => _machine.Fire(Trigger.Analyze);
public void Assign() => _machine.Fire(Trigger.Assign);
public void Resolve() => _machine.Fire(Trigger.Resolve);
public void Confirm() => _machine.Fire(Trigger.Confirm);
public void Reanalyze() => _machine.Fire(Trigger.Reanalyze);
public void Reject() => _machine.Fire(Trigger.Reject);
public void Defer() => _machine.Fire(Trigger.Defer);
public void Resume() => _machine.Fire(Trigger.Resume);
public void Reopen() => _machine.Fire(Trigger.Reopen);
}

public static class Program
{
public static void Main()
{
var bug = new Bug();
Console.WriteLine($"Start: {bug.CurrentState}");

bug.Analyze();
Console.WriteLine($"After Analyze: {bug.CurrentState}");

bug.Assign();
Console.WriteLine($"After Assign: {bug.CurrentState}");

bug.Resolve();
Console.WriteLine($"After Resolve: {bug.CurrentState}");

bug.Confirm();
Console.WriteLine($"After Confirm: {bug.CurrentState}");

bug.Reopen();
Console.WriteLine($"After Reopen: {bug.CurrentState} " +
$"(reopened {bug.ReopenCount} time(s))");

bug.Analyze();
Console.WriteLine($"After Analyze: {bug.CurrentState}");
}
}
}
23 changes: 23 additions & 0 deletions BugTests/BugTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="MSTest" Version="3.6.1" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BugPro\BugPro.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions BugTests/MSTestSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
237 changes: 237 additions & 0 deletions BugTests/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
using System;
using BugPro;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BugTests
{
[TestClass]
public class BugWorkflowTests
{
private static Bug FreshBug() => new Bug();

[TestMethod]
public void NewBug_StartsInOpen()
{
var bug = FreshBug();
Assert.AreEqual(State.Open, bug.CurrentState);
}

[TestMethod]
public void NewBug_HasZeroReopens()
{
var bug = FreshBug();
Assert.AreEqual(0, bug.ReopenCount);
}

[TestMethod]
public void Analyze_MovesOpenToAnalysis()
{
var bug = FreshBug();
bug.Analyze();
Assert.AreEqual(State.InAnalysis, bug.CurrentState);
}

[TestMethod]
public void Assign_MovesAnalysisToInProgress()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
Assert.AreEqual(State.InProgress, bug.CurrentState);
}

[TestMethod]
public void Resolve_MovesInProgressToResolved()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
bug.Resolve();
Assert.AreEqual(State.Resolved, bug.CurrentState);
}

[TestMethod]
public void Confirm_MovesResolvedToClosed()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
bug.Resolve();
bug.Confirm();
Assert.AreEqual(State.Closed, bug.CurrentState);
}

[TestMethod]
public void Reject_MovesAnalysisToRejected()
{
var bug = FreshBug();
bug.Analyze();
bug.Reject();
Assert.AreEqual(State.Rejected, bug.CurrentState);
}

[TestMethod]
public void Defer_MovesAnalysisToDeferred()
{
var bug = FreshBug();
bug.Analyze();
bug.Defer();
Assert.AreEqual(State.Deferred, bug.CurrentState);
}

[TestMethod]
public void Resume_MovesDeferredBackToAnalysis()
{
var bug = FreshBug();
bug.Analyze();
bug.Defer();
bug.Resume();
Assert.AreEqual(State.InAnalysis, bug.CurrentState);
}

[TestMethod]
public void Reanalyze_FromInProgress_ReturnsToAnalysis()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
bug.Reanalyze();
Assert.AreEqual(State.InAnalysis, bug.CurrentState);
}

[TestMethod]
public void Reanalyze_FromResolved_ReturnsToAnalysis()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
bug.Resolve();
bug.Reanalyze();
Assert.AreEqual(State.InAnalysis, bug.CurrentState);
}

[TestMethod]
public void Reopen_FromClosed_MovesToReopened()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
bug.Resolve();
bug.Confirm();
bug.Reopen();
Assert.AreEqual(State.Reopened, bug.CurrentState);
}

[TestMethod]
public void Reopen_FromRejected_MovesToReopened()
{
var bug = FreshBug();
bug.Analyze();
bug.Reject();
bug.Reopen();
Assert.AreEqual(State.Reopened, bug.CurrentState);
}

[TestMethod]
public void Reopen_IncrementsReopenCount()
{
var bug = FreshBug();
bug.Analyze();
bug.Reject();
bug.Reopen();
Assert.AreEqual(1, bug.ReopenCount);
}

[TestMethod]
public void Reopened_Analyze_ReturnsToAnalysis()
{
var bug = FreshBug();
bug.Analyze();
bug.Reject();
bug.Reopen();
bug.Analyze();
Assert.AreEqual(State.InAnalysis, bug.CurrentState);
}

[TestMethod]
public void CanFire_Analyze_TrueInOpen()
{
var bug = FreshBug();
Assert.IsTrue(bug.CanFire(Trigger.Analyze));
}

[TestMethod]
public void CanFire_Assign_FalseInOpen()
{
var bug = FreshBug();
Assert.IsFalse(bug.CanFire(Trigger.Assign));
}

[TestMethod]
public void Assign_FromOpen_Throws()
{
var bug = FreshBug();
Assert.ThrowsException<InvalidOperationException>(() => bug.Assign());
}

[TestMethod]
public void Confirm_FromOpen_Throws()
{
var bug = FreshBug();
Assert.ThrowsException<InvalidOperationException>(() => bug.Confirm());
}

[TestMethod]
public void Resolve_FromAnalysis_Throws()
{
var bug = FreshBug();
bug.Analyze();
Assert.ThrowsException<InvalidOperationException>(() => bug.Resolve());
}

[TestMethod]
public void Reopen_FromOpen_Throws()
{
var bug = FreshBug();
Assert.ThrowsException<InvalidOperationException>(() => bug.Reopen());
}

[TestMethod]
public void Resume_FromClosed_Throws()
{
var bug = FreshBug();
bug.Analyze();
bug.Assign();
bug.Resolve();
bug.Confirm();
Assert.ThrowsException<InvalidOperationException>(() => bug.Resume());
}

[TestMethod]
public void Reopen_BeyondLimit_Throws()
{
var bug = FreshBug();
bug.Analyze();
bug.Reject();

for (int i = 0; i < Bug.MaxReopen; i++)
{
bug.Reopen();
bug.Analyze();
bug.Reject();
}

Assert.AreEqual(Bug.MaxReopen, bug.ReopenCount);
Assert.IsFalse(bug.CanFire(Trigger.Reopen));
Assert.ThrowsException<InvalidOperationException>(() => bug.Reopen());
}

[TestMethod]
public void DoubleAnalyze_Throws()
{
var bug = FreshBug();
bug.Analyze();
Assert.ThrowsException<InvalidOperationException>(() => bug.Analyze());
}
}
}
Loading
Loading