зеркало из https://github.com/Azure/benchpress.git
spike/engine lifecycle (#28)
* 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:
Родитель
479f542ea6
Коммит
e6e2ca23bf
|
@ -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() { }
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
```
|
Загрузка…
Ссылка в новой задаче