* add Mermaid formatted sequence diagram

* add draft engine interfaces and sequence diagram

* A post-create script that gets executed when the container first launches

* Example of using Aspect-oriented programming to start test engine

* wired up engine lifecycle manager

* reformat to C# bracket style

* add additional lifecycle hooks for more granualarity

* Create StateMachineExample.cs

* WIP state machine

* Lifecycle diagram

* API, example test, state diagram.

* close to done on example code diagram and doc

* More examples and text updates

* Update engine_lifecycle_spike.md

* minor refactoring

* minor refactorings

* minor refactorings

* minor refactorings, add awaits

* update markdown doc and minor refactoring to example code

* remove unused code

* update doc

* Make sure code compiles. Minor updates to wording

* refactoring and aligning doc to code

* delete duplicate code in /sequence_diagram.md

* delete unused code

* update casing to match convention

* comment strings added to design class

* remove unused code, rename main

* move Attributes folder to designs

Co-authored-by: Uffaz <uffaz@Uffazs-MacBook-Pro.local>
Co-authored-by: Uffaz Nathaniel <ThePedestrian@users.noreply.github.com>
This commit is contained in:
Robert David Hernandez 2022-11-11 15:00:11 -06:00 коммит произвёл GitHub
Родитель 479f542ea6
Коммит e6e2ca23bf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 471 добавлений и 2 удалений

Просмотреть файл

@ -3,5 +3,4 @@ ARG VARIANT="6.0"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:${VARIANT}
COPY ./scripts/post-create.sh /benchpress/
RUN chmod +x /benchpress/post-create.sh
RUN chmod +x /benchpress/post-create.sh

Просмотреть файл

@ -0,0 +1,4 @@
public class BenchpressTestAttribute : Attribute
{
public BenchpressTestAttribute() { }
}

Просмотреть файл

@ -0,0 +1,4 @@
public class OnDoneAttribute : Attribute
{
public OnDoneAttribute() { }
}

Просмотреть файл

@ -0,0 +1,4 @@
public class OnEngineStartFailureAttribute : Attribute
{
public OnEngineStartFailureAttribute() { }
}

Просмотреть файл

@ -0,0 +1,4 @@
public class OnEngineStartSuccessAttribute : Attribute
{
public OnEngineStartSuccessAttribute() { }
}

Просмотреть файл

@ -0,0 +1,5 @@
public class OnInitizationAttribute : Attribute
{
public OnInitizationAttribute() { }
}

Просмотреть файл

@ -0,0 +1,4 @@
public class OnShutdownAttribute : Attribute
{
public OnShutdownAttribute() { }
}

Просмотреть файл

@ -0,0 +1,4 @@
public class OnTestExecuteAttribute : Attribute
{
public OnTestExecuteAttribute() { }
}

238
designs/BenchPress.cs Normal file
Просмотреть файл

@ -0,0 +1,238 @@
using System.Reflection;
public enum State
{
PreInitialization,
Initialization,
EngineStarting,
EngineStartSuccess,
EngineStartFailure,
TestExecute,
Shutdown,
EngineShutdownSuccess,
Done
}
// The choice of name of the class is not significant and is subject
// to change by the desginer.
//
// At time of writing, this class is not intended to be in a
// compilable or executable state.
//
// The objective of this class design artifact is serve as a guide
// for a developer to programatically manage the lifecycle of the
// BenchPress Engine and the State Machine it will implement.
public class BenchPress : ILifecycleManager
{
private static readonly object lockObj = new object();
private static BenchPress? instance = null;
private const int MaxRestart = 2;
public int EnginePID { get; private set; } = -1;
public State CurrentState { get; private set; } = State.PreInitialization;
public static BenchPress Instance
{
get
{
if (instance == null)
{
lock (lockObj)
{
instance = new BenchPress();
}
}
return instance;
}
}
private BenchPress()
{
TransitionToNextState(State.Initialization);
}
private void Process()
{
switch (CurrentState)
{
case State.Initialization: Init(); break;
case State.EngineStarting:
PreEngineStart();
StartEngine();
break;
case State.EngineStartSuccess: OnEngineStartSuccess(); break;
case State.EngineStartFailure: OnEngineStartFailure(); break;
case State.TestExecute: OnTestExecute(); break;
case State.Shutdown: StopEngine(); break;
case State.EngineShutdownSuccess: Teardown(); break;
case State.Done: Done(); break;
}
}
private void TransitionToNextState(State nextState)
{
if (nextState != CurrentState)
{
CurrentState = nextState;
Process();
}
}
public void Init()
{
TransitionToNextState(State.EngineStarting);
}
public void PreEngineStart(int retryCount=3, int httpTimeout=60000, bool keepAlive=true) { }
public async void StartEngine()
{
var isStarted = false;
try
{
Monitor.Enter(lockObj);
try
{
var restartCount = 0;
while (!isStarted && restartCount < MAX_RESTART)
{
restartCount++;
int enginePID = await MockStartProcess();
if (enginePID > 0)
{
isStarted = true;
}
}
}
finally
{
Monitor.Exit(lockObj);
}
}
catch (SynchronizationLockException SyncEx)
{
Console.WriteLine("A SynchronizationLockException occurred. Message: ");
Console.WriteLine(SyncEx.Message);
}
if (isStarted)
{
TransitionToNextState(State.EngineStartSuccess);
}
else
{
TransitionToNextState(State.EngineStartFailure);
}
}
private void OnEngineStartSuccess()
{
TransitionToNextState(State.TestExecute);
}
private void OnEngineStartFailure()
{
TransitionToNextState(State.Shutdown);
}
private void PreTestExecute() { }
private void OnTestExecute()
{
var testMethods = GetMethodsWithAttribute(typeof(BenchpressTestAttribute));
// Invoke the test
testMethods.ToList().ForEach(method =>
{
InvokeTest(method);
});
TransitionToNextState(State.Shutdown);
}
public async void StopEngine()
{
try
{
Monitor.Enter(lockObj);
try
{
int exitCode = await MockStopProcess(EnginePID);
}
finally
{
Monitor.Exit(lockObj);
}
}
catch (SynchronizationLockException SyncEx)
{
Console.WriteLine("A SynchronizationLockException occurred. Message: ");
Console.WriteLine(SyncEx.Message);
}
TransitionToNextState(State.EngineShutdownSuccess);
}
private void Teardown()
{
InvokeMethodsMarkedWithAttribute(typeof(OnShutdownAttribute));
TransitionToNextState(State.Done);
}
private void Done() { }
private void InvokeTest(MethodInfo method)
{
// pre
//InvokeMethodsMarkedWithAttribute(BenchPress.Attributes.PreTestExecute);
// execute
//InvokeMethodsMarkedWithAttribute(BenchPress.Attributes.Test);
// post
InvokeMethodsMarkedWithAttribute(typeof(OnTestExecuteAttribute));
}
private MethodInfo[] GetMethodsWithAttribute(params Type[] attributes)
{
return new MethodInfo[] { };
}
private void InvokeMethodsMarkedWithAttribute(params Type[] attributes)
{
/* logic */
/*foreach (Attribute attr in attributes)
{
foreach (MethodInfo method in attr)
{
if (HasAttribute(method, attr))
{
method.Invoke();
}
}
}*/
}
private async Task<int> MockStartProcess()
{
int PID = -1;
await Task.Run(() =>
{
PID = 1;
});
return PID;
}
private async Task<int> MockStopProcess(int PID)
{
int EXIT_CODE = -1;
await Task.Run(() =>
{
EXIT_CODE = 0;
});
return EXIT_CODE;
}
}

Просмотреть файл

@ -0,0 +1,3 @@
interface IAllocateResourceGroup {
bool CreateResourceGroup(String AzureSubscritionId);
}

Просмотреть файл

@ -0,0 +1,3 @@
interface ICreateResource {
bool CreateResources(String resourceGroupName, List<String> resources);
}

Просмотреть файл

@ -0,0 +1,6 @@
interface ILifecycleManager {
void Init();
void PreEngineStart(int retryCount=3, int httpTimeout=60000, bool keepAlive=true);
void StartEngine();
void StopEngine();
}

Просмотреть файл

@ -0,0 +1,3 @@
interface IValidateResource {
bool ValidateResoure(String validStateJson);
}

188
designs/README.md Normal file
Просмотреть файл

@ -0,0 +1,188 @@
# Benchpress Testing Framework Design
## Overview
The core Benchpress engine proposes an *Inversion of Control* (IoC) design paradigm. In this design, we transfer the control of objects or portions of a program to a container or framework for orchestrating and execution.
Few advantages of IoC design is that it enables test writers to use any language-specific framework of their choice. For example, in C# *NUnit*, *XUnit*, and *MSTest* are some of the most popular choices.
The primary goal of the Benchpress testing framework is to start and stop the *Core testing Engine* in a thread-safe manner, while also supporting injecting optional runtime configuration into the engine.
Some examples of runtime configuration that the framework may provide to the engine include:
- Number of automatic restarts due to exception conditions the engine should undergo before reporting a fatal error to the framework.
- Number of automatic retries should the engine make when interacting over the network before reporting a fatal error to the framework.
- How long of a time period should the engine wait during network interactions before entering a timeout condition and reporting a fatal error to the framework.
Our API allows test authors to include declarative decorators/annotations/attributes on their test methods and/or test classes that will intercept the flow-of-control at runtime to execute the various pre/post lifecycle management steps - including ensuring an engine process instance is available - allowing for massively concurrent test execution.
## State Diagram
Internally the testing framework has a state machine that allows it to track and execute events and test methods.
```mermaid
flowchart TD
PreInitialization --> Initialization
Initialization --> EngineStarting
EngineStarting -->|Success| EngineStartSuccess
EngineStarting -->|Failure| EngineStartFailure
EngineStartSuccess --> TestExecute
TestExecute -->|1...n| TestExecute
EngineStartFailure --> ShutDown
TestExecute --> ShutDown
ShutDown --> EngineShutdownSuccess
EngineShutdownSuccess --> Done
```
## Public API
The public API **only** needs to be called in frameworks/languages where IoC is not possible, e.g., Powershell/Pester. In such cases, the expectation is that users will call `Start()` to configure the testing enviornment and on test teardown call `Stop()`.
- `Init()` - Init singleton instance
- `PreEngineStart` - Optional Configuration
- `StartEngine()` - Start the engine
- `StopEngine()` - Stop the engine
## Example Test
### Method-level
```c#
class Test1
{
// Annotate at method level
[BenchpressTest]
public void TestRG()
{
var result = Benchpress.DoesResourceGroupExist("my-rg");
Debug.Assert(result, true);
}
// Annotate at method level
[BenchpressTest]
public void Test_ResourceGroupExists()
{
var result = Benchpress.DoesResourceGroupExist("my-rg");
Debug.Assert(result, true);
}
}
```
### Class-level
```c#
// Annotate at class level
[BenchpressTest]
class Test2
{
public void TestRG()
{
var result = Benchpress.DoesResourceGroupExist("my-rg");
Debug.Assert(result, true);
}
public void Test_ResourceGroupExists()
{
var result = Benchpress.DoesResourceGroupExist("my-rg");
Debug.Assert(result, true);
}
}
```
### Subscribe to life-cycle events
```c#
// Listen to life-cycle events
[BenchpressTest]
class Test3
{
[Initization]
public void OnInitization() { /* ... */ }
[EngineStartSuccess]
public void OnEngineStartSuccess() { /* ... */ }
[EngineStartFailure]
public void OnEngineStartFailure() { /* ... */ }
[TestExecute]
public void OnTestExecute(MethodInfo method) { /* ... */ }
[Shutdown]
public void OnShutdown() { /* ... */ }
[Done]
public void OnDone() { /* ... */ }
[Fact]
public void TestResourceGroupExists()
{
var result = Benchpress.DoesStorageAccountExists("mystorage");
Debug.Assert(result == true, "Test failed");
}
[Fact]
public void TestStorageAccountPolicy()
{
var result = Benchpress.DoesStorageAccountExists("mystorage--222");
Debug.Assert(result == true, "Test failed");
}
}
```
### Lifecycle Event Descriptions
```
Init() // Called once at the very start of test execution before the engine is started. This method can be used to perform or pre-execution steps.
PreEngineStart(RetryCount = 3, HttpTimeout = 60000, KeepAlive = true, ...more configuration) – Called once to start the engine if not already started
StartEngine() // Performs resource locking and starts the engine
OnEngineStartSuccess() // Called once if the engine is started successfully
OnEngineStartFailure() // Called once the engine is started successfully.
PreTestExecute(method) // Called once before each test executes.
OnTestExecute(method) // Called once after the test executes.
PreTestExecute(method) // Called once before each test executes.
StopEngine() // Performs resource locking and stops the engine
TearDown() // Called once after the engine is stopped and before Done() is called
Done() // Called at the end once of framework
```
### Proposed Sequence Diagram
```mermaid
sequenceDiagram
Runtime/OS->>+Engine: main([...args])
Engine->Engine: Init
Engine->Engine: If RestartCount>2 then Destroy
Engine->Engine: Setup
par Engine Ready State. ASync Comms allowed
Runtime/OS->>+Engine: Shutdown Signal
Engine->Engine: Destroy
Engine-->>-Runtime/OS: $EXIT_CODE
and
Engine->Engine: Exceptional Condition
Engine->Engine: Teardown
and
Framework->>+Engine: Create Resource Group given $AZURE_SUBSCRIPTION_ID
Engine->>+Azure: Create Resource Group
Azure-->>-Engine:
Engine->>-Framework: Resource Group Creation Status
and
Framework->>+Engine: Create Resource(s) given $RESOURCE_GROUP_NAME
Engine->>+Azure: Create Resource(s)
Azure-->>-Engine:
Engine-->>-Framework:
and
Framework->>+Engine: Validate Resource(s) given $VALID_STAE
Engine->>+Azure: Get Resource(s) State(s)
Azure-->>-Engine:
Engine-->>-Framework:
end
Engine->Engine: Teardown
Engine->Engine: If AutoRestart==True then Setup
Engine->Engine: Destroy
Engine-->>-Runtime/OS: $EXIT_CODE
```