diff --git a/REstate.sln b/REstate.sln index f1e0ecc..5cf1b4a 100644 --- a/REstate.sln +++ b/REstate.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2003 +VisualStudioVersion = 15.0.27130.2020 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "REstate", "src\REstate\REstate.csproj", "{8B02FF53-A9F3-4004-8CDC-32831070957D}" EndProject @@ -29,6 +29,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "REstate.Remote.Tests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Semaphore", "src\Examples\Semaphore\Semaphore.csproj", "{F15E390F-EFCF-4848-96C8-597BD75B7F9C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "REstate.Engine.Repositories.EntityFrameworkCore", "src\REstate.Engine.Repositories.EntityFrameworkCore\REstate.Engine.Repositories.EntityFrameworkCore.csproj", "{3E0263FE-540E-401E-818A-CB51350D30C2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "REstate.Engine.Repositories.EntityFrameworkCore.Tests", "test\REstate.Engine.Repositories.EntityFrameworkCore.Tests\REstate.Engine.Repositories.EntityFrameworkCore.Tests.csproj", "{5F424F7B-4840-414D-9FA3-7700D81AD898}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,6 +75,14 @@ Global {F15E390F-EFCF-4848-96C8-597BD75B7F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {F15E390F-EFCF-4848-96C8-597BD75B7F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F15E390F-EFCF-4848-96C8-597BD75B7F9C}.Release|Any CPU.Build.0 = Release|Any CPU + {3E0263FE-540E-401E-818A-CB51350D30C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E0263FE-540E-401E-818A-CB51350D30C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E0263FE-540E-401E-818A-CB51350D30C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E0263FE-540E-401E-818A-CB51350D30C2}.Release|Any CPU.Build.0 = Release|Any CPU + {5F424F7B-4840-414D-9FA3-7700D81AD898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F424F7B-4840-414D-9FA3-7700D81AD898}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F424F7B-4840-414D-9FA3-7700D81AD898}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F424F7B-4840-414D-9FA3-7700D81AD898}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,6 +98,8 @@ Global {7E243087-D506-4D89-BB66-83C6DE063778} = {88B4B54C-3DA1-40D2-8DCB-0181DD035955} {C64BFCFB-3BF8-4662-A274-7ACC324232CD} = {88B4B54C-3DA1-40D2-8DCB-0181DD035955} {F15E390F-EFCF-4848-96C8-597BD75B7F9C} = {20BCC0A3-8FEB-4EFF-A175-22D1E3E4E233} + {3E0263FE-540E-401E-818A-CB51350D30C2} = {90E8D167-048D-467B-81D5-1007247226A3} + {5F424F7B-4840-414D-9FA3-7700D81AD898} = {88B4B54C-3DA1-40D2-8DCB-0181DD035955} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0C35B35D-40C1-4521-BF0A-C5A6D7BFF4CD} diff --git a/src/REstate.Engine.Connectors.AzureServiceBus/REstate.Engine.Connectors.AzureServiceBus.csproj b/src/REstate.Engine.Connectors.AzureServiceBus/REstate.Engine.Connectors.AzureServiceBus.csproj index 292531a..f206a4d 100644 --- a/src/REstate.Engine.Connectors.AzureServiceBus/REstate.Engine.Connectors.AzureServiceBus.csproj +++ b/src/REstate.Engine.Connectors.AzureServiceBus/REstate.Engine.Connectors.AzureServiceBus.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreEngineRepositoryContext.cs b/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreEngineRepositoryContext.cs new file mode 100644 index 0000000..c4f9088 --- /dev/null +++ b/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreEngineRepositoryContext.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.EntityFrameworkCore; +using REstate.Schematics; + +namespace REstate.Engine.Repositories.EntityFrameworkCore +{ + public class EntityFrameworkCoreEngineRepositoryContext + : IEngineRepositoryContext + , ISchematicRepository + , IMachineRepository + { + public EntityFrameworkCoreEngineRepositoryContext(REstateDbContext dbContext) + { + DbContext = dbContext; + } + + + public ISchematicRepository Schematics => this; + public IMachineRepository Machines => this; + + protected REstateDbContext DbContext { get; } + + /// + /// Retrieves a previously stored Schematic by name. + /// + /// The name of the Schematic + /// + /// Thrown if is null. + /// Thrown when no matching Schematic was found for the given name. + public async Task> RetrieveSchematicAsync(string schematicName, CancellationToken cancellationToken = default) + { + if (schematicName == null) throw new ArgumentNullException(nameof(schematicName)); + + EntityFrameworkCoreSchematic result; + try + { + result = await DbContext.Schematics.SingleAsync( + schematicRecord => schematicRecord.SchematicName == schematicName, + cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + throw new SchematicDoesNotExistException(schematicName); + } + + var schematic = MessagePackSerializer.Deserialize>( + bytes: MessagePackSerializer.FromJson(result.SchematicJson), + resolver: ContractlessStandardResolver.Instance); + + return schematic; + } + + /// + /// Stores a Schematic, using its SchematicName as the key. + /// + /// The Schematic to store + /// + /// Thrown if is null. + /// Thrown if has a null SchematicName property. + public async Task> StoreSchematicAsync(Schematic schematic, CancellationToken cancellationToken = default) + { + if (schematic == null) throw new ArgumentNullException(nameof(schematic)); + if (schematic.SchematicName == null) throw new ArgumentException("Schematic must have a name to be stored.", nameof(schematic)); + + + var schematicJson = MessagePackSerializer.ToJson( + obj: schematic, + resolver: ContractlessStandardResolver.Instance); + + var record = new EntityFrameworkCoreSchematic + { + SchematicName = schematic.SchematicName, + SchematicJson = schematicJson + }; + + DbContext.Schematics.Add(record); + + await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return schematic; + } + + /// + /// Creates a new Machine from a provided Schematic. + /// + /// The name of the stored Schematic + /// The Id of Machine to create; if null, an Id will be generated. + /// Related metadata for the Machine + /// + /// Thrown if is null. + public async Task> CreateMachineAsync(string schematicName, string machineId, IDictionary metadata, + CancellationToken cancellationToken = default) + { + if (schematicName == null) throw new ArgumentNullException(nameof(schematicName)); + + var schematic = await RetrieveSchematicAsync(schematicName, cancellationToken).ConfigureAwait(false); + + return await CreateMachineAsync(schematic, machineId, metadata, cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a new Machine from a provided Schematic. + /// + /// The Schematic of the Machine + /// The Id of Machine to create; if null, an Id will be generated. + /// Related metadata for the Machine + /// + /// Thrown if is null. + /// Thrown when no matching Schematic was found for the given name. + public async Task> CreateMachineAsync( + Schematic schematic, + string machineId, + IDictionary metadata, + CancellationToken cancellationToken = default) + { + if (schematic == null) throw new ArgumentNullException(nameof(schematic)); + + var id = machineId ?? Guid.NewGuid().ToString(); + + var schematicJson = MessagePackSerializer.ToJson( + obj: schematic, + resolver: ContractlessStandardResolver.Instance); + + var stateJson = MessagePackSerializer.ToJson( + obj: schematic.InitialState, + resolver: ContractlessStandardResolver.Instance); + + string metadataJson = null; + if (metadata != null) + metadataJson = MessagePackSerializer.ToJson( + obj: metadata); + + var commitTag = Guid.NewGuid(); + var updatedTime = DateTimeOffset.UtcNow; + + var record = new EntityFrameworkCoreMachineStatus + { + MachineId = id, + SchematicJson = schematicJson, + StateJson = stateJson, + CommitTag = commitTag, + UpdatedTime = updatedTime, + MetadataJson = metadataJson + }; + + DbContext.Machines.Add(record); + + await DbContext.SaveChangesAsync(cancellationToken); + + return new MachineStatus + { + MachineId = id, + Schematic = schematic.Clone(), + State = schematic.InitialState, + Metadata = metadata, + CommitTag = commitTag, + UpdatedTime = updatedTime + }; + } + + public async Task>> BulkCreateMachinesAsync( + Schematic schematic, + IEnumerable> metadata, + CancellationToken cancellationToken = default) + { + if (schematic == null) throw new ArgumentNullException(nameof(schematic)); + + var schematicJson = MessagePackSerializer.ToJson( + obj: schematic, + resolver: ContractlessStandardResolver.Instance); + + var stateJson = MessagePackSerializer.ToJson( + obj: schematic.InitialState, + resolver: ContractlessStandardResolver.Instance); + + var commitTag = Guid.NewGuid(); + var updatedTime = DateTimeOffset.UtcNow; + + var records = new List(); + var machineStatuses = new List>(); + + foreach (var dictionary in metadata) + { + var machineId = Guid.NewGuid().ToString(); + + string metadataJson = null; + if (dictionary != null) + metadataJson = MessagePackSerializer.ToJson( + obj: dictionary); + + records.Add(new EntityFrameworkCoreMachineStatus + { + MachineId = machineId, + SchematicJson = schematicJson, + StateJson = stateJson, + CommitTag = commitTag, + UpdatedTime = updatedTime, + MetadataJson = metadataJson + }); + + machineStatuses.Add(new MachineStatus + { + MachineId = machineId, + Schematic = schematic.Clone(), + State = schematic.InitialState, + Metadata = dictionary, + CommitTag = commitTag, + UpdatedTime = updatedTime + }); + } + + await DbContext.AddRangeAsync(records, cancellationToken); + + await DbContext.SaveChangesAsync(cancellationToken); + + return machineStatuses; + } + + public async Task>> BulkCreateMachinesAsync( + string schematicName, + IEnumerable> metadata, + CancellationToken cancellationToken = default) + { + var schematic = await RetrieveSchematicAsync(schematicName, cancellationToken).ConfigureAwait(false); + + return await BulkCreateMachinesAsync(schematic, metadata, cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a Machine. + /// + /// + /// Does not throw an exception if a matching Machine was not found. + /// + /// The Id of the Machine to delete + /// + /// Thrown if is null. + public async Task DeleteMachineAsync(string machineId, CancellationToken cancellationToken = default) + { + if (machineId == null) throw new ArgumentNullException(nameof(machineId)); + + var machineRecord = await DbContext.Machines + .SingleOrDefaultAsync( + status => status.MachineId == machineId, + cancellationToken).ConfigureAwait(false); + + if (machineRecord != null) + { + DbContext.Machines.Remove(machineRecord); + + await DbContext.SaveChangesAsync(cancellationToken); + } + } + + /// + /// Retrieves the record for a Machine Status. + /// + /// The Id of the Machine + /// + /// Thrown if is null. + /// Thrown when no matching MachineId was found. + public async Task> GetMachineStatusAsync( + string machineId, + CancellationToken cancellationToken = default) + { + if (machineId == null) throw new ArgumentNullException(nameof(machineId)); + + var machineRecord = await DbContext.Machines + .SingleOrDefaultAsync( + status => status.MachineId == machineId, + cancellationToken).ConfigureAwait(false); + + if(machineRecord == null) throw new MachineDoesNotExistException(machineId); + + var schematic = MessagePackSerializer.Deserialize>( + bytes: MessagePackSerializer.FromJson(machineRecord.SchematicJson), + resolver: ContractlessStandardResolver.Instance); + + var state = MessagePackSerializer.Deserialize( + bytes: MessagePackSerializer.FromJson(machineRecord.StateJson), + resolver: ContractlessStandardResolver.Instance); + + IDictionary metadata = null; + if(machineRecord.MetadataJson != null) + metadata = MessagePackSerializer.Deserialize>( + bytes: MessagePackSerializer.FromJson(machineRecord.MetadataJson)); + + return new MachineStatus + { + MachineId = machineId, + Schematic = schematic, + State = state, + Metadata = metadata, + CommitTag = machineRecord.CommitTag, + UpdatedTime = machineRecord.UpdatedTime + }; + } + + /// + /// Updates the Status record of a Machine + /// + /// The Id of the Machine + /// The state to which the Status is set. + /// + /// If provided, will guarentee the update will occur only + /// if the value matches the current Status's CommitTag. + /// + /// + /// Thrown if is null. + /// Thrown when no matching MachineId was found. + /// Thrown when a conflict occured on CommitTag; no update was performed. + public async Task> SetMachineStateAsync( + string machineId, + TState state, + Guid? lastCommitTag, + CancellationToken cancellationToken = default) + { + if (machineId == null) throw new ArgumentNullException(nameof(machineId)); + + var machineRecord = await DbContext.Machines + .SingleOrDefaultAsync( + status => status.MachineId == machineId, + cancellationToken).ConfigureAwait(false); + + if(machineRecord == null) throw new MachineDoesNotExistException(machineId); + + if (lastCommitTag == null || machineRecord.CommitTag == lastCommitTag) + { + var stateJson = MessagePackSerializer.ToJson( + obj: state, + resolver: ContractlessStandardResolver.Instance); + + machineRecord.StateJson = stateJson; + machineRecord.CommitTag = Guid.NewGuid(); + machineRecord.UpdatedTime = DateTimeOffset.UtcNow; + } + else + { + throw new StateConflictException(); + } + + try + { + await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException ex) + { + throw new StateConflictException(ex); + } + + var schematic = MessagePackSerializer.Deserialize>( + bytes: MessagePackSerializer.FromJson(machineRecord.SchematicJson), + resolver: ContractlessStandardResolver.Instance); + + IDictionary metadata = null; + if(machineRecord.MetadataJson != null) + metadata = MessagePackSerializer.Deserialize>( + bytes: MessagePackSerializer.FromJson(machineRecord.MetadataJson)); + + return new MachineStatus + { + MachineId = machineId, + Schematic = schematic, + State = state, + Metadata = metadata, + CommitTag = machineRecord.CommitTag, + UpdatedTime = machineRecord.UpdatedTime + }; + } + + #region IDisposable Support + // To detect redundant calls + private bool _disposedValue; + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + DbContext.Dispose(); + } + + _disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + + } +} diff --git a/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreRepositoryComponent.cs b/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreRepositoryComponent.cs new file mode 100644 index 0000000..d68399e --- /dev/null +++ b/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreRepositoryComponent.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore; +using REstate.IoC; + +namespace REstate.Engine.Repositories.EntityFrameworkCore +{ + /// + /// REstate component that configures Entity Framework Core as the state storage system. + /// + public class EntityFrameworkCoreRepositoryComponent + : IComponent + { + private readonly REstateEntityFrameworkCoreServerOptions _options; + + /// + /// Creates an instance of the component given configurationusing a . /> + /// + /// The connection options REstate will use + public EntityFrameworkCoreRepositoryComponent(Action optionsBuilder) + { + var builder = new DbContextOptionsBuilder(); + + optionsBuilder?.Invoke(builder); + + _options = new REstateEntityFrameworkCoreServerOptions(builder.Options); + } + + /// + /// Registers the dependencies of this component and actives necessary configuration + /// + /// The registrar to which the configuration and dependencies should be added + public void Register(IRegistrar registrar) + { + registrar.Register(_options); + registrar.Register(typeof(IRepositoryContextFactory<,>), typeof(EntityFrameworkCoreRepositoryContextFactory<,>)); + } + } + +} diff --git a/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreRepositoryContextFactory.cs b/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreRepositoryContextFactory.cs new file mode 100644 index 0000000..664753f --- /dev/null +++ b/src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreRepositoryContextFactory.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace REstate.Engine.Repositories.EntityFrameworkCore +{ + + public class EntityFrameworkCoreRepositoryContextFactory + : IRepositoryContextFactory + { + internal EntityFrameworkCoreRepositoryContextFactory(REstateEntityFrameworkCoreServerOptions options) + { + Options = options; + } + + private REstateEntityFrameworkCoreServerOptions Options { get; } + + public Task> OpenContextAsync( + CancellationToken cancellationToken = default) + { + return Task.FromResult>( + new EntityFrameworkCoreEngineRepositoryContext( + new REstateDbContext(Options.DbContextOptions))); + } + } + +} diff --git a/src/REstate.Engine.Repositories.EntityFrameworkCore/REstate.Engine.Repositories.EntityFrameworkCore.csproj b/src/REstate.Engine.Repositories.EntityFrameworkCore/REstate.Engine.Repositories.EntityFrameworkCore.csproj new file mode 100644 index 0000000..4691b39 --- /dev/null +++ b/src/REstate.Engine.Repositories.EntityFrameworkCore/REstate.Engine.Repositories.EntityFrameworkCore.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + latest + 2.0.0 + true + + + + + + + + + + + + + diff --git a/src/REstate.Engine.Repositories.EntityFrameworkCore/REstateDbContext.cs b/src/REstate.Engine.Repositories.EntityFrameworkCore/REstateDbContext.cs new file mode 100644 index 0000000..4c98012 --- /dev/null +++ b/src/REstate.Engine.Repositories.EntityFrameworkCore/REstateDbContext.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace REstate.Engine.Repositories.EntityFrameworkCore +{ + public class EntityFrameworkCoreMachineStatus + { + public string MachineId { get; set; } + + public Guid CommitTag { get; set; } + + public DateTimeOffset UpdatedTime { get; set; } + + public string StateJson { get; set; } + + public string MetadataJson { get; set; } + + public string SchematicJson { get; set; } + } + + public class EntityFrameworkCoreSchematic + { + public string SchematicName { get; set; } + + public string SchematicJson { get; set; } + } + + public class REstateDbContext + : DbContext + { + public REstateDbContext(DbContextOptions options) + : base(options) + { + } + + /// + /// Override this method to further configure the model that was discovered by convention from the entity types + /// exposed in properties on your derived context. The resulting model may be cached + /// and re-used for subsequent instances of your derived context. + /// + /// + /// If a model is explicitly set on the options for this context + /// (via ) + /// then this method will not be run. + /// + /// + /// The builder being used to construct the model for this context. Databases (and other extensions) typically + /// define extension methods on this object that allow you to configure aspects of the model that are specific + /// to a given database. + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(status => status.MachineId); + + modelBuilder.Entity() + .Property(status => status.CommitTag) + .IsConcurrencyToken(); + + modelBuilder.Entity() + .HasKey(schematic => schematic.SchematicName); + } + + public DbSet Machines { get; set; } + + public DbSet Schematics { get; set; } + } +} diff --git a/src/REstate.Engine.Repositories.EntityFrameworkCore/REstateEntityFrameworkCoreServerOptions.cs b/src/REstate.Engine.Repositories.EntityFrameworkCore/REstateEntityFrameworkCoreServerOptions.cs new file mode 100644 index 0000000..dc95162 --- /dev/null +++ b/src/REstate.Engine.Repositories.EntityFrameworkCore/REstateEntityFrameworkCoreServerOptions.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; + +namespace REstate.Engine.Repositories.EntityFrameworkCore +{ + internal class REstateEntityFrameworkCoreServerOptions + { + public REstateEntityFrameworkCoreServerOptions(DbContextOptions dbContextOptions) + { + DbContextOptions = dbContextOptions; + } + + public DbContextOptions DbContextOptions { get; } + } + +} diff --git a/src/REstate.Engine.Repositories.Redis/REstate.Engine.Repositories.Redis.csproj b/src/REstate.Engine.Repositories.Redis/REstate.Engine.Repositories.Redis.csproj index ecec229..278158e 100644 --- a/src/REstate.Engine.Repositories.Redis/REstate.Engine.Repositories.Redis.csproj +++ b/src/REstate.Engine.Repositories.Redis/REstate.Engine.Repositories.Redis.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/REstate.Remote/GrpcRemoteHostComponent.cs b/src/REstate.Remote/GrpcRemoteHostComponent.cs index 11d1979..c412112 100644 --- a/src/REstate.Remote/GrpcRemoteHostComponent.cs +++ b/src/REstate.Remote/GrpcRemoteHostComponent.cs @@ -1,6 +1,4 @@ using System; -using Grpc.Core; -using MagicOnion.Client; using REstate.Engine; using REstate.Remote.Services; using REstate.IoC; diff --git a/src/REstate.Remote/GrpcStateEngine.cs b/src/REstate.Remote/GrpcStateEngine.cs index acc6325..f243e72 100644 --- a/src/REstate.Remote/GrpcStateEngine.cs +++ b/src/REstate.Remote/GrpcStateEngine.cs @@ -95,17 +95,18 @@ public async Task> CreateMachineAsync( return new GrpcStateMachine(_stateMachineService, response.MachineId); } - public Task BulkCreateMachinesAsync( + public Task>> BulkCreateMachinesAsync( ISchematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default) => BulkCreateMachinesAsync(schematic.Clone(), metadata, cancellationToken); - public async Task BulkCreateMachinesAsync( + public async Task>> BulkCreateMachinesAsync( Schematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default) - => await _stateMachineService + { + var response = await _stateMachineService .WithCancellationToken(cancellationToken) .BulkCreateMachineFromSchematicAsync(new BulkCreateMachineFromSchematicRequest { @@ -113,11 +114,15 @@ public async Task BulkCreateMachinesAsync( Metadata = metadata }); - public async Task BulkCreateMachinesAsync( + return response.MachineIds.Select(machineId => new GrpcStateMachine(_stateMachineService, machineId)); + } + + public async Task>> BulkCreateMachinesAsync( string schematicName, IEnumerable> metadata, CancellationToken cancellationToken = default) - => await _stateMachineService + { + var response = await _stateMachineService .WithCancellationToken(cancellationToken) .BulkCreateMachineFromStoreAsync(new BulkCreateMachineFromStoreRequest { @@ -125,6 +130,9 @@ public async Task BulkCreateMachinesAsync( Metadata = metadata }); + return response.MachineIds.Select(machineId => new GrpcStateMachine(_stateMachineService, machineId)); + } + public async Task DeleteMachineAsync( string machineId, CancellationToken cancellationToken = default) diff --git a/src/REstate.Remote/Models/BulkCreateMachine.cs b/src/REstate.Remote/Models/BulkCreateMachine.cs index 889a951..8e525f1 100644 --- a/src/REstate.Remote/Models/BulkCreateMachine.cs +++ b/src/REstate.Remote/Models/BulkCreateMachine.cs @@ -22,4 +22,11 @@ public class BulkCreateMachineFromSchematicRequest [Key(1)] public IEnumerable> Metadata { get; set; } } + + [MessagePackObject] + public class BulkCreateMachineResponse + { + [Key(0)] + public IEnumerable MachineIds { get; set; } + } } \ No newline at end of file diff --git a/src/REstate.Remote/REstate.Remote.csproj b/src/REstate.Remote/REstate.Remote.csproj index 3c7dfe7..61c7562 100644 --- a/src/REstate.Remote/REstate.Remote.csproj +++ b/src/REstate.Remote/REstate.Remote.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/src/REstate.Remote/Services/IStateMachineService.cs b/src/REstate.Remote/Services/IStateMachineService.cs index f320ffb..ea71a3a 100644 --- a/src/REstate.Remote/Services/IStateMachineService.cs +++ b/src/REstate.Remote/Services/IStateMachineService.cs @@ -25,8 +25,8 @@ public interface IStateMachineService UnaryResult CreateMachineFromSchematicAsync(CreateMachineFromSchematicRequest request); - UnaryResult BulkCreateMachineFromStoreAsync(BulkCreateMachineFromStoreRequest request); + UnaryResult BulkCreateMachineFromStoreAsync(BulkCreateMachineFromStoreRequest request); - UnaryResult BulkCreateMachineFromSchematicAsync(BulkCreateMachineFromSchematicRequest request); + UnaryResult BulkCreateMachineFromSchematicAsync(BulkCreateMachineFromSchematicRequest request); } } \ No newline at end of file diff --git a/src/REstate.Remote/Services/IStateMachineServiceLocalAdapter.cs b/src/REstate.Remote/Services/IStateMachineServiceLocalAdapter.cs index e9ffadf..c85f623 100644 --- a/src/REstate.Remote/Services/IStateMachineServiceLocalAdapter.cs +++ b/src/REstate.Remote/Services/IStateMachineServiceLocalAdapter.cs @@ -9,8 +9,8 @@ namespace REstate.Remote.Services { internal interface IStateMachineServiceLocalAdapter { - Task BulkCreateMachineFromSchematicAsync(Schematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default); - Task BulkCreateMachineFromStoreAsync(string schematicName, IEnumerable> metadata, CancellationToken cancellationToken = default); + Task BulkCreateMachineFromSchematicAsync(Schematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default); + Task BulkCreateMachineFromStoreAsync(string schematicName, IEnumerable> metadata, CancellationToken cancellationToken = default); Task CreateMachineFromSchematicAsync(Schematic schematic, string machineId, IDictionary metadata, CancellationToken cancellationToken = default); Task CreateMachineFromStoreAsync(string schematicName, string machineId, IDictionary metadata, CancellationToken cancellationToken = default); Task DeleteMachineAsync(string machineId, CancellationToken cancellationToken); diff --git a/src/REstate.Remote/Services/StateMachineService.cs b/src/REstate.Remote/Services/StateMachineService.cs index 541b5a7..78d2dd8 100644 --- a/src/REstate.Remote/Services/StateMachineService.cs +++ b/src/REstate.Remote/Services/StateMachineService.cs @@ -25,8 +25,8 @@ namespace REstate.Remote.Services using GetMachineMetadataAsyncDelegate = Func>; using CreateMachineFromStoreAsyncDelegate = Func, CancellationToken, Task>; using CreateMachineFromSchematicAsyncDelegate = Func, CancellationToken, Task>; - using BulkCreateMachineFromStoreAsyncDelegate = Func>, CancellationToken, Task>; - using BulkCreateMachineFromSchematicAsyncDelegate = Func>, CancellationToken, Task>; + using BulkCreateMachineFromStoreAsyncDelegate = Func>, CancellationToken, Task>; + using BulkCreateMachineFromSchematicAsyncDelegate = Func>, CancellationToken, Task>; using GetSchematicAsyncDelegate = Func>; using DeleteMachineAsyncDelegate = Func; @@ -373,7 +373,7 @@ public async UnaryResult CreateMachineFromSchematicAsync( GetCallCancellationToken()); } - public async UnaryResult BulkCreateMachineFromStoreAsync(BulkCreateMachineFromStoreRequest request) + public async UnaryResult BulkCreateMachineFromStoreAsync(BulkCreateMachineFromStoreRequest request) { var genericTypes = GetGenericsFromHeaders(); @@ -408,12 +408,10 @@ public async UnaryResult BulkCreateMachineFromStoreAsync(BulkCreateMachineF .Compile(); }); - await bulkCreateMachineFromStoreAsync(request.SchematicName, request.Metadata, GetCallCancellationToken()); - - return Nil.Default; + return await bulkCreateMachineFromStoreAsync(request.SchematicName, request.Metadata, GetCallCancellationToken()); } - public async UnaryResult BulkCreateMachineFromSchematicAsync(BulkCreateMachineFromSchematicRequest request) + public async UnaryResult BulkCreateMachineFromSchematicAsync(BulkCreateMachineFromSchematicRequest request) { var genericTypes = GetGenericsFromHeaders(); @@ -454,9 +452,7 @@ public async UnaryResult BulkCreateMachineFromSchematicAsync(BulkCreateMach .Compile(); }); - await bulkCreateMachineFromSchematicAsync(schematic, request.Metadata, GetCallCancellationToken()); - - return Nil.Default; + return await bulkCreateMachineFromSchematicAsync(schematic, request.Metadata, GetCallCancellationToken()); } public async UnaryResult GetSchematicAsync(GetSchematicRequest request) diff --git a/src/REstate.Remote/Services/StateMachineServiceLocalAdapter.cs b/src/REstate.Remote/Services/StateMachineServiceLocalAdapter.cs index 4d03b29..6b945fc 100644 --- a/src/REstate.Remote/Services/StateMachineServiceLocalAdapter.cs +++ b/src/REstate.Remote/Services/StateMachineServiceLocalAdapter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Grpc.Core; @@ -190,7 +191,7 @@ public async Task CreateMachineFromSchematicAsync( + public async Task BulkCreateMachineFromStoreAsync( string schematicName, IEnumerable> metadata, CancellationToken cancellationToken = default) @@ -199,10 +200,15 @@ public async Task BulkCreateMachineFromStoreAsync( .AsLocal() .GetStateEngine(); - await engine.BulkCreateMachinesAsync(schematicName, metadata, cancellationToken); + var machines = await engine.BulkCreateMachinesAsync(schematicName, metadata, cancellationToken); + + return new BulkCreateMachineResponse + { + MachineIds = machines.Select(machine => machine.MachineId) + }; } - public async Task BulkCreateMachineFromSchematicAsync( + public async Task BulkCreateMachineFromSchematicAsync( Schematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default) @@ -211,7 +217,12 @@ public async Task BulkCreateMachineFromSchematicAsync( .AsLocal() .GetStateEngine(); - await engine.BulkCreateMachinesAsync(schematic, metadata, cancellationToken); + var machines = await engine.BulkCreateMachinesAsync(schematic, metadata, cancellationToken); + + return new BulkCreateMachineResponse + { + MachineIds = machines.Select(machine => machine.MachineId) + }; } public async Task GetSchematicAsync( diff --git a/src/REstate/Engine/IStateEngine.cs b/src/REstate/Engine/IStateEngine.cs index 05e6dd9..6e1e2df 100644 --- a/src/REstate/Engine/IStateEngine.cs +++ b/src/REstate/Engine/IStateEngine.cs @@ -40,17 +40,17 @@ Task> CreateMachineAsync( IDictionary metadata = null, CancellationToken cancellationToken = default); - Task BulkCreateMachinesAsync( + Task>> BulkCreateMachinesAsync( Schematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default); - Task BulkCreateMachinesAsync( + Task>> BulkCreateMachinesAsync( ISchematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default); - Task BulkCreateMachinesAsync( + Task>> BulkCreateMachinesAsync( string schematicName, IEnumerable> metadata, CancellationToken cancellationToken = default); diff --git a/src/REstate/Engine/StateEngine.cs b/src/REstate/Engine/StateEngine.cs index bd18b1e..d5a51a9 100644 --- a/src/REstate/Engine/StateEngine.cs +++ b/src/REstate/Engine/StateEngine.cs @@ -218,7 +218,7 @@ private void NotifyOnMachineCreated( #pragma warning restore 4014 } - public Task BulkCreateMachinesAsync( + public Task>> BulkCreateMachinesAsync( ISchematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default) @@ -226,7 +226,7 @@ public Task BulkCreateMachinesAsync( return BulkCreateMachinesAsync(schematic.Clone(), metadata, cancellationToken); } - public async Task BulkCreateMachinesAsync( + public async Task>> BulkCreateMachinesAsync( Schematic schematic, IEnumerable> metadata, CancellationToken cancellationToken = default) @@ -242,19 +242,21 @@ public async Task BulkCreateMachinesAsync( var machines = machineStatuses - .Select(status => ValueTuple.Create( - item1: _stateMachineFactory + .Select(status => ( + machine: _stateMachineFactory .ConstructFromSchematic( status.MachineId, schematic, new ReadOnlyDictionary( status.Metadata ?? new Dictionary(0))), - item2: status)) + status)) .ToList(); NotifyBulkOnMachineCreated(schematic, machineStatuses); await BulkCallOnInitialEntryAction(schematic, machines, cancellationToken); + + return machines.Select(tuple => tuple.machine); } private void NotifyBulkOnMachineCreated( @@ -276,7 +278,7 @@ private void NotifyBulkOnMachineCreated( #pragma warning restore 4014 } - public async Task BulkCreateMachinesAsync( + public async Task>> BulkCreateMachinesAsync( string schematicName, IEnumerable> metadata, CancellationToken cancellationToken = default) @@ -296,20 +298,21 @@ public async Task BulkCreateMachinesAsync( } var machines = machineStatuses - .Select(status => ValueTuple.Create( - item1: _stateMachineFactory + .Select(status => ( + machine: _stateMachineFactory .ConstructFromSchematic( machineId: status.MachineId, schematic: schematic, metadata: new ReadOnlyDictionary( status.Metadata ?? new Dictionary(0))), - item2: status)) + status)) .ToList(); NotifyBulkOnMachineCreated(schematic, machineStatuses); await BulkCallOnInitialEntryAction(schematic, machines, cancellationToken); + return machines.Select(tuple => tuple.machine); } private async Task BulkCallOnInitialEntryAction( diff --git a/test/ManualTestCases.playlist b/test/ManualTestCases.playlist new file mode 100644 index 0000000..6b646d3 --- /dev/null +++ b/test/ManualTestCases.playlist @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/Context/REstateEntityFrameworkCoreContext.cs b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/Context/REstateEntityFrameworkCoreContext.cs new file mode 100644 index 0000000..02c2744 --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/Context/REstateEntityFrameworkCoreContext.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using REstate.Tests.Features.Context; + +namespace REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context +{ + public class REstateEntityFrameworkCoreContext + : REstateContext + { + #region GIVEN + public Task Given_EntityFrameworkCore_is_the_registered_repository() + { + CurrentHost.Agent().Configuration + .RegisterComponent(new EntityFrameworkCoreRepositoryComponent(builder => builder.UseInMemoryDatabase("REstateScenarioTests"))); + + return Task.CompletedTask; + } + #endregion + + #region WHEN + + #endregion + + #region THEN + + #endregion + } +} diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineCreation.cs b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineCreation.cs new file mode 100644 index 0000000..68f1b91 --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineCreation.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context; +using REstate.Tests.Features.Templates; + +// ReSharper disable InconsistentNaming + +namespace REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features +{ + [FeatureDescription(@" +In order to use familiar storage +As a developer +I want to create Machines from Schematics stored using Entity Framework Core")] + [ScenarioCategory("Machine Creation")] + [ScenarioCategory("EntityFrameworkCore")] + public class MachineCreation + : MachineCreationScenarios> + { + protected override Task Given_host_configuration_is_applied() + { + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .AddAsyncSteps( + _ => _.Given_EntityFrameworkCore_is_the_registered_repository()) + .Build()); + } + } +} diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineDeletion.cs b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineDeletion.cs new file mode 100644 index 0000000..6995e66 --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineDeletion.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context; +using REstate.Tests.Features.Templates; + +// ReSharper disable InconsistentNaming + +namespace REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features +{ + [FeatureDescription(@" +In order to use familiar storage +As a developer +I want to delete previously created Machines stored using Entity Framework Core")] + [ScenarioCategory("Machine Deletion")] + [ScenarioCategory("EntityFrameworkCore")] + public class MachineDeletion + : MachineDeletionScenarios> + { + protected override Task Given_host_configuration_is_applied() + { + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .AddAsyncSteps( + _ => _.Given_EntityFrameworkCore_is_the_registered_repository()) + .Build()); + } + } +} diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineRetrieval.cs b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineRetrieval.cs new file mode 100644 index 0000000..cc563d2 --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineRetrieval.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context; +using REstate.Tests.Features.Templates; + +// ReSharper disable InconsistentNaming + +namespace REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features +{ + [FeatureDescription(@" +In order to use familiar storage +As a developer +I want to retrieve previously created Machines stored using Entity Framework Core")] + [ScenarioCategory("Machine Retrieval")] + [ScenarioCategory("EntityFrameworkCore")] + public class MachineRetrieval + : MachineRetrievalScenarios> + { + protected override Task Given_host_configuration_is_applied() + { + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .AddAsyncSteps( + _ => _.Given_EntityFrameworkCore_is_the_registered_repository()) + .Build()); + } + } +} diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Properties/AssemblyInfo.cs b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..86a9819 --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using LightBDD.XUnit2; + +[assembly:LightBddScope] diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/ProviderValidationTests.cs b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/ProviderValidationTests.cs new file mode 100644 index 0000000..fb0fb5a --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/ProviderValidationTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace REstate.Engine.Repositories.EntityFrameworkCore.Tests +{ + /// + /// Tests providers to ensure they support required features for REstate. + /// + public class ProviderValidationTests + { + public static object[][] Providers = + { + new object[] { new Provider("InMemory", builder => builder.UseInMemoryDatabase("InMemory")) }, + + // Uncomment below to run provider specific tests that require external databases + /* + new object[] { new Provider("SqlServer", builder => builder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=REstateEfTests;Trusted_Connection=True;MultipleActiveResultSets=true")) } + */ + }; + + [Theory] + [MemberData(nameof(Providers))] + public async Task ConcurrencyCheckIsSupported(Provider provider) + { + // Arrange + var contextFactory = new SampleDbContextFactory(provider.Action); + + var id = Guid.NewGuid(); + + using (var dbContext = contextFactory.CreateContext()) + { + await dbContext.Database.EnsureDeletedAsync(); + await dbContext.Database.EnsureCreatedAsync(); + + dbContext.Data.Add(new Datum + { + Id = id, + Counter = 0, + CommitTag = Guid.NewGuid() + }); + + await dbContext.SaveChangesAsync(); + } + + async Task ModifyDataAsync() + { + while (true) + { + using (var dbContext = contextFactory.CreateContext()) + { + var entity = await dbContext.Data.SingleAsync(datum => datum.Id == id); + + entity.Counter++; + entity.CommitTag = Guid.NewGuid(); + + try + { + await dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + continue; + } + + break; + } + } + } + + // Act + var t1 = Task.Run(async () => await ModifyDataAsync()); + var t2 = Task.Run(async () => await ModifyDataAsync()); + var t3 = Task.Run(async () => await ModifyDataAsync()); + var t4 = Task.Run(async () => await ModifyDataAsync()); + + await Task.WhenAll(t1, t2, t3, t4); + + Datum result; + using (var dbContext = contextFactory.CreateContext()) + { + result = await dbContext.Data.SingleAsync(datum => datum.Id == id); + } + + // Assert + + Assert.Equal(4, result.Counter); + } + + public class SampleDbContextFactory + { + private DbContextOptions Options { get; } + + public SampleDbContextFactory(Action providerAction) + { + var builder = new DbContextOptionsBuilder(); + + providerAction(builder); + + Options = builder.Options; + } + + public SampleDbContext CreateContext() + { + return new SampleDbContext(Options); + } + } + + public class SampleDbContext + : DbContext + { + public SampleDbContext(DbContextOptions options) + : base(options) + { + + } + + public DbSet Data { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(datum => datum.Id); + + modelBuilder.Entity() + .Property(datum => datum.CommitTag) + .IsConcurrencyToken(); + } + } + + public class Datum + { + public Guid Id { get; set; } + + public int Counter { get; set; } + + public Guid CommitTag { get; set; } + } + + public class Provider + { + public Provider(string name, Action action) + { + Name = name; + Action = action; + } + + public string Name { get; } + + public Action Action { get; } + + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + { + return Name; + } + } + } +} diff --git a/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/REstate.Engine.Repositories.EntityFrameworkCore.Tests.csproj b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/REstate.Engine.Repositories.EntityFrameworkCore.Tests.csproj new file mode 100644 index 0000000..2d92c87 --- /dev/null +++ b/test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/REstate.Engine.Repositories.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,33 @@ + + + + netcoreapp2.0 + + false + + + + latest + + + + latest + + + + + + + + + + + + + + + + + + + diff --git a/test/REstate.Remote.Tests/Features/Context/REstateRemoteContext.cs b/test/REstate.Remote.Tests/Features/Context/REstateRemoteContext.cs index 41a24fd..66bb001 100644 --- a/test/REstate.Remote.Tests/Features/Context/REstateRemoteContext.cs +++ b/test/REstate.Remote.Tests/Features/Context/REstateRemoteContext.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using Grpc.Core; using REstate.Tests.Features.Context; using Xunit; @@ -20,6 +17,7 @@ public static class SharedRemoteContext public class REstateRemoteContext : REstateContext { + #region GIVEN public Task Given_a_REstate_gRPC_Server_running() { if (CurrentGrpcServer == null) @@ -40,6 +38,25 @@ public Task Given_a_REstate_gRPC_Server_running() return Task.CompletedTask; } + public Task Given_the_default_agent_is_gRPC_remote() + { + CurrentHost.Agent().Configuration + .RegisterComponent(new GrpcRemoteHostComponent(new GrpcHostOptions + { + Channel = new Channel("localhost", CurrentGrpcServer.BoundPorts[0], ChannelCredentials.Insecure), + UseAsDefaultEngine = true + })); + + return Task.CompletedTask; + } + + public async Task Given_a_REstate_gRPC_Server_failure() + { + await CurrentGrpcServer.KillAsync(); + } + #endregion + + #region WHEN public Task When_a_REstate_gRPC_Server_is_created_and_started() { if (CurrentGrpcServer == null) @@ -59,19 +76,9 @@ public Task When_a_REstate_gRPC_Server_is_created_and_started() return Task.CompletedTask; } + #endregion - public Task Given_the_default_agent_is_gRPC_remote() - { - CurrentHost.Agent().Configuration - .RegisterComponent(new GrpcRemoteHostComponent(new GrpcHostOptions - { - Channel = new Channel("localhost", CurrentGrpcServer.BoundPorts[0], ChannelCredentials.Insecure), - UseAsDefaultEngine = true - })); - - return Task.CompletedTask; - } - + #region THEN public Task Then_REstate_gRPC_Server_has_bound_ports() { Assert.NotNull(CurrentGrpcServer); @@ -80,10 +87,6 @@ public Task Then_REstate_gRPC_Server_has_bound_ports() return Task.CompletedTask; } - - public async Task Given_a_REstate_gRPC_Server_failure() - { - await CurrentGrpcServer.KillAsync(); - } + #endregion } } diff --git a/test/REstate.Remote.Tests/Features/GrpcServer.cs b/test/REstate.Remote.Tests/Features/GrpcServer.cs index 87ec5b5..55d12e6 100644 --- a/test/REstate.Remote.Tests/Features/GrpcServer.cs +++ b/test/REstate.Remote.Tests/Features/GrpcServer.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using LightBDD.Framework; using LightBDD.Framework.Scenarios.Contextual; using LightBDD.Framework.Scenarios.Extended; using LightBDD.XUnit2; using REstate.Remote.Tests.Features.Context; +using REstate.Tests.Features.Templates; using Xunit; -using Xunit.Abstractions; namespace REstate.Remote.Tests.Features { @@ -21,22 +18,15 @@ As a developer [ScenarioCategory("gRPC")] [Collection("gRPC")] public class GrpcServer - : FeatureFixture + : REstateFeature> { [Scenario] public async Task REstate_gRPC_Server_Sets_BoundPorts_on_start() { - await Runner.WithContext>().RunScenarioAsync( + await Runner.WithContext(Context).RunScenarioAsync( _ => _.Given_a_new_host(), _ => _.When_a_REstate_gRPC_Server_is_created_and_started(), _ => _.Then_REstate_gRPC_Server_has_bound_ports()); } - - #region Constructor - public GrpcServer(ITestOutputHelper output) - : base(output) - { - } - #endregion } } diff --git a/test/REstate.Remote.Tests/Features/MachineCreation.cs b/test/REstate.Remote.Tests/Features/MachineCreation.cs index 3742caa..0427753 100644 --- a/test/REstate.Remote.Tests/Features/MachineCreation.cs +++ b/test/REstate.Remote.Tests/Features/MachineCreation.cs @@ -1,13 +1,9 @@ -using System; -using System.Reflection; using System.Threading.Tasks; using LightBDD.Framework; using LightBDD.Framework.Scenarios.Contextual; using LightBDD.Framework.Scenarios.Extended; -using LightBDD.XUnit2; using REstate.Remote.Tests.Features.Context; -using Xunit; -using Xunit.Abstractions; +using REstate.Tests.Features.Templates; // ReSharper disable InconsistentNaming @@ -21,87 +17,18 @@ As a developer [ScenarioCategory("Remote")] [ScenarioCategory("gRPC")] public class MachineCreation - : FeatureFixture + : MachineCreationScenarios> { - [Scenario] - public async Task A_Machine_can_be_created_from_a_Schematic() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_REstate_gRPC_Server_running(), - _ => _.Given_the_default_agent_is_gRPC_remote(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.When_a_Machine_is_created_from_a_Schematic(_.CurrentSchematic), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); - } - - [Scenario] - public async Task A_Machine_can_be_created_from_a_SchematicName() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_REstate_gRPC_Server_running(), - _ => _.Given_the_default_agent_is_gRPC_remote(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), - _ => _.When_a_Machine_is_created_from_a_SchematicName(_.CurrentSchematic.SchematicName), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); - } - - [Scenario] - public async Task A_Machine_can_be_created_from_a_Schematic_with_a_predefined_MachineId() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_REstate_gRPC_Server_running(), - _ => _.Given_the_default_agent_is_gRPC_remote(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.When_a_Machine_is_created_from_a_Schematic_with_a_predefined_MachineId(_.CurrentSchematic, machineId), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine), - _ => _.Then_the_MachineId_is_MACHINEID(_.CurrentMachine, machineId)); - } - - [Scenario] - public async Task A_Machine_can_be_created_from_a_SchematicName_with_a_predefined_MachineId() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_REstate_gRPC_Server_running(), - _ => _.Given_the_default_agent_is_gRPC_remote(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), - _ => _.When_a_Machine_is_created_from_a_SchematicName_with_a_predefined_MachineId(_.CurrentSchematic.SchematicName, machineId), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine), - _ => _.Then_the_MachineId_is_MACHINEID(_.CurrentMachine, machineId)); - } - - #region Constructor - public MachineCreation(ITestOutputHelper output) - : base(output) + protected override Task Given_host_configuration_is_applied() { + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .AddAsyncSteps( + _ => _.Given_a_REstate_gRPC_Server_running(), + _ => _.Given_the_default_agent_is_gRPC_remote()) + .Build()); } - #endregion } } diff --git a/test/REstate.Remote.Tests/Features/MachineDeletion.cs b/test/REstate.Remote.Tests/Features/MachineDeletion.cs new file mode 100644 index 0000000..e459451 --- /dev/null +++ b/test/REstate.Remote.Tests/Features/MachineDeletion.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using REstate.Remote.Tests.Features.Context; +using REstate.Tests.Features.Templates; + +// ReSharper disable InconsistentNaming + +namespace REstate.Remote.Tests.Features +{ + [FeatureDescription(@" +In order to support cloud scaling +As a developer +I want to delete Machines from a remote server")] + [ScenarioCategory("Machine Deletion")] + [ScenarioCategory("Remote")] + [ScenarioCategory("gRPC")] + public class MachineDeletion + : MachineDeletionScenarios> + { + protected override Task Given_host_configuration_is_applied() + { + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .AddAsyncSteps( + _ => _.Given_a_REstate_gRPC_Server_running(), + _ => _.Given_the_default_agent_is_gRPC_remote()) + .Build()); + } + } +} diff --git a/test/REstate.Remote.Tests/Features/MachineRetrieval.cs b/test/REstate.Remote.Tests/Features/MachineRetrieval.cs index 8dfb0c2..18013c7 100644 --- a/test/REstate.Remote.Tests/Features/MachineRetrieval.cs +++ b/test/REstate.Remote.Tests/Features/MachineRetrieval.cs @@ -1,14 +1,9 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using System.Threading.Tasks; +using System.Threading.Tasks; using LightBDD.Framework; using LightBDD.Framework.Scenarios.Contextual; using LightBDD.Framework.Scenarios.Extended; -using LightBDD.XUnit2; using REstate.Remote.Tests.Features.Context; -using Xunit; -using Xunit.Abstractions; +using REstate.Tests.Features.Templates; // ReSharper disable InconsistentNaming @@ -22,48 +17,18 @@ As a developer [ScenarioCategory("Remote")] [ScenarioCategory("gRPC")] public class MachineRetrieval - : FeatureFixture - { - [Scenario] - public async Task A_Machine_can_be_retrieved() + : MachineRetrievalScenarios> + { + protected override Task Given_host_configuration_is_applied() { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_REstate_gRPC_Server_running(), - _ => _.Given_the_default_agent_is_gRPC_remote(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.Given_a_Machine_exists_with_MachineId_MACHINEID(_.CurrentSchematic, machineId), - _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .AddAsyncSteps( + _ => _.Given_a_REstate_gRPC_Server_running(), + _ => _.Given_the_default_agent_is_gRPC_remote()) + .Build()); } - - [Scenario] - public async Task A_NonExistant_Machine_cannot_be_retrieved() - { - var uniqueId = Guid.NewGuid().ToString(); - - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_REstate_gRPC_Server_running(), - _ => _.Given_the_default_agent_is_gRPC_remote(), - _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), - _ => _.Then_MachineDoesNotExistException_is_thrown()); - } - - #region Constructor - public MachineRetrieval(ITestOutputHelper output) - : base(output) - { - } - #endregion } } - diff --git a/test/REstate.Remote.Tests/REstate.Remote.Tests.csproj b/test/REstate.Remote.Tests/REstate.Remote.Tests.csproj index 1938291..a54ee0d 100644 --- a/test/REstate.Remote.Tests/REstate.Remote.Tests.csproj +++ b/test/REstate.Remote.Tests/REstate.Remote.Tests.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/test/REstate.Remote.Tests/Units/StateMachineServiceTests.cs b/test/REstate.Remote.Tests/Units/StateMachineServiceTests.cs index e5bf49a..b027a89 100644 --- a/test/REstate.Remote.Tests/Units/StateMachineServiceTests.cs +++ b/test/REstate.Remote.Tests/Units/StateMachineServiceTests.cs @@ -7,7 +7,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MagicOnion.Server; @@ -28,8 +28,8 @@ public StateMachineServiceTests() _localAdapterMock = new Mock(MockBehavior.Strict); _stateMachineServiceMock = new Mock( - MockBehavior.Strict, - new ConcurrentDictionary(), + MockBehavior.Strict, + new ConcurrentDictionary(), _localAdapterMock.Object); _stateMachineServiceMock @@ -149,7 +149,7 @@ public async Task SendWithPayloadAsync() public async Task StoreSchematicAsync() { // Arrange - var schematic = new Schematic{ SchematicName = "schematic name" }; + var schematic = new Schematic { SchematicName = "schematic name" }; var schematicBytes = MessagePackSerializer.Serialize(schematic, ContractlessStandardResolver.Instance); _localAdapterMock @@ -182,14 +182,14 @@ public async Task GetMachineSchematicAsync() _localAdapterMock .Setup(_ => _.GetMachineSchematicAsync( - It.Is(it => it == machineId), + It.Is(it => it == machineId), It.IsAny())) .ReturnsAsync(new GetMachineSchematicResponse { MachineId = machineId, SchematicBytes = schematicBytes }); - + // Act var response = await _stateMachineServiceMock.Object .GetMachineSchematicAsync(new GetMachineSchematicRequest @@ -330,7 +330,11 @@ public async Task BulkCreateMachineFromStoreAsync() It.Is(it => it == schematicName), It.IsAny>>(), It.IsAny())) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult( + new BulkCreateMachineResponse + { + MachineIds = new[] { "a", "b" } + })); // Act var response = await _stateMachineServiceMock.Object @@ -347,6 +351,11 @@ public async Task BulkCreateMachineFromStoreAsync() metadataEnumerable, CancellationToken.None), times: Times.Once()); + + Assert.NotNull(response); + Assert.NotNull(response.MachineIds); + Assert.Collection(response.MachineIds, Assert.NotNull, Assert.NotNull); + Assert.Collection(response.MachineIds, machineId => Assert.Equal("a", machineId), machineId => Assert.Equal("b", machineId)); } [Fact] @@ -375,7 +384,11 @@ public async Task BulkCreateMachineFromSchematicAsync() It.Is>(it => it.SchematicName == schematic.SchematicName), It.IsAny>>(), It.IsAny())) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult( + new BulkCreateMachineResponse + { + MachineIds = new[] { "a", "b" } + })); // Act var response = await _stateMachineServiceMock.Object @@ -389,10 +402,15 @@ public async Task BulkCreateMachineFromSchematicAsync() // Note: first parameter has to be checked for It.Is, other parameters CANNOT or the verify will fail _localAdapterMock.Verify( expression: _ => _.BulkCreateMachineFromSchematicAsync( - It.Is>(it => it.SchematicName == schematic.SchematicName), - metadataEnumerable, + It.Is>(it => it.SchematicName == schematic.SchematicName), + metadataEnumerable, CancellationToken.None), times: Times.Once()); + + Assert.NotNull(response); + Assert.NotNull(response.MachineIds); + Assert.Collection(response.MachineIds, Assert.NotNull, Assert.NotNull); + Assert.Collection(response.MachineIds, machineId => Assert.Equal("a", machineId), machineId => Assert.Equal("b", machineId)); } [Fact] diff --git a/test/REstate.Tests/Features/Configuration.cs b/test/REstate.Tests/Features/Configuration.cs index fab8c53..83df323 100644 --- a/test/REstate.Tests/Features/Configuration.cs +++ b/test/REstate.Tests/Features/Configuration.cs @@ -5,7 +5,6 @@ using LightBDD.XUnit2; using REstate.IoC.BoDi; using REstate.Tests.Features.Context; -using Xunit.Abstractions; // ReSharper disable InconsistentNaming @@ -42,12 +41,5 @@ await Runner.WithContext().RunScenarioAsync( _ => _.Then_configuration_is_not_null(), _ => _.Then_configuration_has_a_container()); } - - #region Constructor - public Configuration(ITestOutputHelper output) - : base(output) - { - } - #endregion } } diff --git a/test/REstate.Tests/Features/Context/Configuration.cs b/test/REstate.Tests/Features/Context/Configuration.cs index 71c8d1f..5b94dc8 100644 --- a/test/REstate.Tests/Features/Context/Configuration.cs +++ b/test/REstate.Tests/Features/Context/Configuration.cs @@ -8,6 +8,11 @@ public partial class REstateContext { public IHostConfiguration CurrentHostConfiguration { get; set; } + #region GIVEN + + #endregion + + #region WHEN public Task When_configuration_is_accessed() { try @@ -21,7 +26,9 @@ public Task When_configuration_is_accessed() return Task.CompletedTask; } + #endregion + #region THEN public Task Then_configuration_has_a_container() { Assert.NotNull(((HostConfiguration)CurrentHostConfiguration).Container); @@ -35,5 +42,7 @@ public Task Then_configuration_is_not_null() return Task.CompletedTask; } + #endregion + } } \ No newline at end of file diff --git a/test/REstate.Tests/Features/Context/Host.cs b/test/REstate.Tests/Features/Context/Host.cs index 4d85623..ae56477 100644 --- a/test/REstate.Tests/Features/Context/Host.cs +++ b/test/REstate.Tests/Features/Context/Host.cs @@ -11,6 +11,7 @@ public partial class REstateContext public Exception CurrentException { get; set; } + #region GIVEN public Task Given_a_new_host() { CurrentHost = new REstateHost(); @@ -24,12 +25,19 @@ public Task Given_a_new_host_with_custom_ComponentContainer(IComponentContainer return Task.CompletedTask; } + #endregion + #region WHEN + + #endregion + + #region THEN public Task Then_no_exception_was_thrown() { Assert.Null(CurrentException); return Task.CompletedTask; } + #endregion } } \ No newline at end of file diff --git a/test/REstate.Tests/Features/Context/Machines.cs b/test/REstate.Tests/Features/Context/Machines.cs index e0d7600..6bda4d9 100644 --- a/test/REstate.Tests/Features/Context/Machines.cs +++ b/test/REstate.Tests/Features/Context/Machines.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using REstate.Engine; using REstate.Schematics; @@ -11,6 +13,18 @@ public partial class REstateContext { public IStateMachine CurrentMachine { get; set; } + public List> BulkCreatedMachines { get; set; } + + #region GIVEN + public async Task Given_a_Machine_exists_with_MachineId_MACHINEID(Schematic schematic, string machineId) + { + CurrentMachine = await CurrentHost.Agent() + .GetStateEngine() + .CreateMachineAsync(schematic, machineId); + } + #endregion + + #region WHEN public async Task When_a_Machine_is_created_from_a_Schematic(Schematic schematic) { try @@ -40,6 +54,34 @@ public async Task When_a_Machine_is_created_from_a_SchematicName(string schemati } + public async Task When_MACHINECOUNT_Machines_are_bulk_created_from_a_SchematicName(string schematicName, int machineCount) + { + try + { + BulkCreatedMachines = (await CurrentHost.Agent() + .GetStateEngine() + .BulkCreateMachinesAsync(schematicName, Enumerable.Repeat(new Dictionary(0), machineCount))).ToList(); + } + catch (Exception ex) + { + CurrentException = ex; + } + } + + public async Task When_MACHINECOUNT_Machines_are_bulk_created_from_a_Schematic(ISchematic schematic, int machineCount) + { + try + { + BulkCreatedMachines = (await CurrentHost.Agent() + .GetStateEngine() + .BulkCreateMachinesAsync(schematic, Enumerable.Repeat(new Dictionary(0), machineCount))).ToList(); + } + catch (Exception ex) + { + CurrentException = ex; + } + } + public async Task When_a_Machine_is_created_from_a_Schematic_with_a_predefined_MachineId( Schematic schematic, string machineId) { @@ -70,6 +112,37 @@ public async Task When_a_Machine_is_created_from_a_SchematicName_with_a_predefin } } + public async Task When_a_Machine_is_retrieved_with_MachineId_MACHINEID(string machineId) + { + try + { + CurrentMachine = null; + CurrentMachine = await CurrentHost.Agent() + .GetStateEngine() + .GetMachineAsync(machineId); + } + catch (Exception ex) + { + CurrentException = ex; + } + } + + public async Task When_a_Machine_is_deleted_with_MachineId_MACHINEID(string machineId) + { + try + { + await CurrentHost.Agent() + .GetStateEngine() + .DeleteMachineAsync(machineId); + } + catch (Exception ex) + { + CurrentException = ex; + } + } + #endregion + + #region THEN public Task Then_the_Machine_is_valid(IStateMachine machine) { Assert.NotNull(machine); @@ -79,33 +152,33 @@ public Task Then_the_Machine_is_valid(IStateMachine machine) return Task.CompletedTask; } - public Task Then_the_MachineId_is_MACHINEID(IStateMachine machine, string machineId) + public Task Then_MACHINECOUNT_Machines_were_created( + List> bulkCreatedMachines, + int machineCount) { - Assert.Equal(machineId, CurrentMachine.MachineId); + Assert.NotNull(bulkCreatedMachines); + Assert.Equal(machineCount, bulkCreatedMachines.Count); return Task.CompletedTask; } - public async Task Given_a_Machine_exists_with_MachineId_MACHINEID(Schematic schematic, string machineId) + public Task Then_the_Machines_created_are_valid(List> bulkCreatedMachines) { - CurrentMachine = await CurrentHost.Agent() - .GetStateEngine() - .CreateMachineAsync(schematic, machineId); + bulkCreatedMachines.ForEach(machine => + { + Assert.NotNull(machine); + Assert.NotNull(machine.MachineId); + Assert.NotEmpty(machine.MachineId); + }); + + return Task.CompletedTask; } - public async Task When_a_Machine_is_retrieved_with_MachineId_MACHINEID(string machineId) + public Task Then_the_MachineId_is_MACHINEID(IStateMachine machine, string machineId) { - try - { - CurrentMachine = null; - CurrentMachine = await CurrentHost.Agent() - .GetStateEngine() - .GetMachineAsync(machineId); - } - catch (Exception ex) - { - CurrentException = ex; - } + Assert.Equal(machineId, CurrentMachine.MachineId); + + return Task.CompletedTask; } public Task Then_MachineDoesNotExistException_is_thrown() @@ -115,5 +188,6 @@ public Task Then_MachineDoesNotExistException_is_thrown() return Task.CompletedTask; } + #endregion } } diff --git a/test/REstate.Tests/Features/Context/Schematics.cs b/test/REstate.Tests/Features/Context/Schematics.cs index c952608..f69e660 100644 --- a/test/REstate.Tests/Features/Context/Schematics.cs +++ b/test/REstate.Tests/Features/Context/Schematics.cs @@ -9,6 +9,7 @@ public partial class REstateContext { public Schematic CurrentSchematic { get; set; } + #region GIVEN public Task Given_a_Schematic_with_an_initial_state_INITIALSTATE(string schematicName, TState initialState) { CurrentSchematic = CurrentHost.Agent() @@ -26,5 +27,14 @@ await CurrentHost.Agent() .GetStateEngine() .StoreSchematicAsync(schematic); } + #endregion + + #region WHEN + + #endregion + + #region THEN + + #endregion } } \ No newline at end of file diff --git a/test/REstate.Tests/Features/MachineCreation.cs b/test/REstate.Tests/Features/MachineCreation.cs index 6ef169b..b92c3ae 100644 --- a/test/REstate.Tests/Features/MachineCreation.cs +++ b/test/REstate.Tests/Features/MachineCreation.cs @@ -1,99 +1,13 @@ -using System; -using System.Reflection; -using System.Threading.Tasks; -using LightBDD.Framework; -using LightBDD.Framework.Scenarios.Contextual; -using LightBDD.Framework.Scenarios.Extended; -using LightBDD.XUnit2; -using REstate.Tests.Features.Context; -using Xunit.Abstractions; +using REstate.Tests.Features.Context; +using REstate.Tests.Features.Templates; // ReSharper disable InconsistentNaming namespace REstate.Tests.Features { - [FeatureDescription(@" -In order to utilize REstate's functionality -As a developer -I want to create machines from schematics")] - [ScenarioCategory("Machine Creation")] public class MachineCreation - : FeatureFixture + : MachineCreationScenarios> { - [Scenario] - public async Task A_Machine_can_be_created_from_a_Schematic() - { - var uniqueId = Guid.NewGuid().ToString(); - var schematicName = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.When_a_Machine_is_created_from_a_Schematic(_.CurrentSchematic), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); - } - - [Scenario] - public async Task A_Machine_can_be_created_from_a_SchematicName() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), - _ => _.When_a_Machine_is_created_from_a_SchematicName(_.CurrentSchematic.SchematicName), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); - } - - [Scenario] - public async Task A_Machine_can_be_created_from_a_Schematic_with_a_predefined_MachineId() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.When_a_Machine_is_created_from_a_Schematic_with_a_predefined_MachineId(_.CurrentSchematic, machineId), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine), - _ => _.Then_the_MachineId_is_MACHINEID(_.CurrentMachine, machineId)); - } - - [Scenario] - public async Task A_Machine_can_be_created_from_a_SchematicName_with_a_predefined_MachineId() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), - _ => _.When_a_Machine_is_created_from_a_SchematicName_with_a_predefined_MachineId(_.CurrentSchematic.SchematicName, machineId), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine), - _ => _.Then_the_MachineId_is_MACHINEID(_.CurrentMachine, machineId)); - } - - #region Constructor - public MachineCreation(ITestOutputHelper output) - : base(output) - { - } - #endregion } - - } - diff --git a/test/REstate.Tests/Features/MachineRetrieval.cs b/test/REstate.Tests/Features/MachineRetrieval.cs index b706fee..7f08d8f 100644 --- a/test/REstate.Tests/Features/MachineRetrieval.cs +++ b/test/REstate.Tests/Features/MachineRetrieval.cs @@ -1,62 +1,12 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using System.Threading.Tasks; -using LightBDD.Framework; -using LightBDD.Framework.Scenarios.Contextual; -using LightBDD.Framework.Scenarios.Extended; -using LightBDD.XUnit2; -using REstate.Tests.Features.Context; -using Xunit.Abstractions; +using REstate.Tests.Features.Context; +using REstate.Tests.Features.Templates; // ReSharper disable InconsistentNaming namespace REstate.Tests.Features { - [FeatureDescription(@" -In order to utilize REstate's functionality -As a developer -I want to retrieve previously created machines")] - [ScenarioCategory("Machine Retrieval")] public class MachineRetrieval - : FeatureFixture + : MachineRetrievalScenarios> { - [Scenario] - public async Task A_Machine_can_be_retrieved() - { - var uniqueId = Guid.NewGuid().ToString(); - - var schematicName = uniqueId; - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), - _ => _.Given_a_Machine_exists_with_MachineId_MACHINEID(_.CurrentSchematic, machineId), - _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), - _ => _.Then_no_exception_was_thrown(), - _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); - } - - [Scenario] - public async Task A_NonExistant_Machine_cannot_be_retrieved() - { - var uniqueId = Guid.NewGuid().ToString(); - - var machineId = uniqueId; - - await Runner.WithContext>().RunScenarioAsync( - _ => _.Given_a_new_host(), - _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), - _ => _.Then_MachineDoesNotExistException_is_thrown()); - } - - #region Constructor - public MachineRetrieval(ITestOutputHelper output) - : base(output) - { - } - #endregion } } - diff --git a/test/REstate.Tests/Features/Templates/MachineCreationScenarios.cs b/test/REstate.Tests/Features/Templates/MachineCreationScenarios.cs new file mode 100644 index 0000000..d1fb7e0 --- /dev/null +++ b/test/REstate.Tests/Features/Templates/MachineCreationScenarios.cs @@ -0,0 +1,128 @@ +using System; +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using LightBDD.XUnit2; +using REstate.Tests.Features.Context; + +// ReSharper disable InconsistentNaming + +namespace REstate.Tests.Features.Templates +{ + [FeatureDescription(@" +In order to utilize REstate's functionality +As a developer +I want to create machines from schematics")] + [ScenarioCategory("Machine Creation")] + public abstract class MachineCreationScenarios + : REstateFeature + where TContext : REstateContext, new() + { + [Scenario] + public async Task A_Machine_can_be_created_from_a_Schematic() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.When_a_Machine_is_created_from_a_Schematic(_.CurrentSchematic), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); + } + + [Scenario] + public async Task A_Machine_can_be_created_from_a_SchematicName() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), + _ => _.When_a_Machine_is_created_from_a_SchematicName(_.CurrentSchematic.SchematicName), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); + } + + [Scenario] + public async Task A_Machine_can_be_created_from_a_Schematic_with_a_predefined_MachineId() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + var machineId = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.When_a_Machine_is_created_from_a_Schematic_with_a_predefined_MachineId(_.CurrentSchematic, machineId), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_the_Machine_is_valid(_.CurrentMachine), + _ => _.Then_the_MachineId_is_MACHINEID(_.CurrentMachine, machineId)); + } + + [Scenario] + public async Task A_Machine_can_be_created_from_a_SchematicName_with_a_predefined_MachineId() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + var machineId = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), + _ => _.When_a_Machine_is_created_from_a_SchematicName_with_a_predefined_MachineId(_.CurrentSchematic.SchematicName, machineId), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_the_Machine_is_valid(_.CurrentMachine), + _ => _.Then_the_MachineId_is_MACHINEID(_.CurrentMachine, machineId)); + } + + [Scenario] + public async Task Machines_can_be_bulk_created_from_a_SchematicName() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.Given_a_Schematic_is_stored(_.CurrentSchematic), + _ => _.When_MACHINECOUNT_Machines_are_bulk_created_from_a_SchematicName(_.CurrentSchematic.SchematicName, 5), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_MACHINECOUNT_Machines_were_created(_.BulkCreatedMachines, 5), + _ => _.Then_the_Machines_created_are_valid(_.BulkCreatedMachines)); + } + + [Scenario] + public async Task Machines_can_be_bulk_created_from_a_Schematic() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.When_MACHINECOUNT_Machines_are_bulk_created_from_a_Schematic(_.CurrentSchematic, 5), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_MACHINECOUNT_Machines_were_created(_.BulkCreatedMachines, 5), + _ => _.Then_the_Machines_created_are_valid(_.BulkCreatedMachines)); + } + } +} + diff --git a/test/REstate.Tests/Features/Templates/MachineDeletionScenarios.cs b/test/REstate.Tests/Features/Templates/MachineDeletionScenarios.cs new file mode 100644 index 0000000..27d4612 --- /dev/null +++ b/test/REstate.Tests/Features/Templates/MachineDeletionScenarios.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using LightBDD.XUnit2; +using REstate.Tests.Features.Context; + +// ReSharper disable InconsistentNaming + +namespace REstate.Tests.Features.Templates +{ + [FeatureDescription(@" +In order to utilize REstate's functionality +As a developer +I want to retrieve previously created machines")] + [ScenarioCategory("Machine Retrieval")] + public abstract class MachineRetrievalScenarios + : REstateFeature + where TContext : REstateContext, new() + { + [Scenario] + public async Task A_Machine_can_be_retrieved() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + var machineId = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.Given_a_Machine_exists_with_MachineId_MACHINEID(_.CurrentSchematic, machineId), + _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), + _ => _.Then_no_exception_was_thrown(), + _ => _.Then_the_Machine_is_valid(_.CurrentMachine)); + } + + [Scenario] + public async Task A_NonExistant_Machine_cannot_be_retrieved() + { + var uniqueId = Guid.NewGuid().ToString(); + + var machineId = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), + _ => _.Then_MachineDoesNotExistException_is_thrown()); + } + + [Scenario] + public async Task A_Machine_that_is_deleted_no_longer_exists() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + var machineId = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.Given_a_Machine_exists_with_MachineId_MACHINEID(_.CurrentSchematic, machineId), + _ => _.When_a_Machine_is_deleted_with_MachineId_MACHINEID(machineId), + _ => _.Then_no_exception_was_thrown(), + _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), + _ => _.Then_MachineDoesNotExistException_is_thrown()); + } + } +} diff --git a/test/REstate.Tests/Features/Templates/MachineRetrievalScenarios.cs b/test/REstate.Tests/Features/Templates/MachineRetrievalScenarios.cs new file mode 100644 index 0000000..0246612 --- /dev/null +++ b/test/REstate.Tests/Features/Templates/MachineRetrievalScenarios.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.Framework.Scenarios.Extended; +using LightBDD.XUnit2; +using REstate.Tests.Features.Context; + +// ReSharper disable InconsistentNaming + +namespace REstate.Tests.Features.Templates +{ + [FeatureDescription(@" +In order to remove previous executions or stop machines +As a developer +I want to delete previously created machines")] + [ScenarioCategory("Machine Deletion")] + public abstract class MachineDeletionScenarios + : REstateFeature + where TContext : REstateContext, new() + { + [Scenario] + public async Task A_Machine_that_is_deleted_no_longer_exists() + { + var uniqueId = Guid.NewGuid().ToString(); + + var schematicName = uniqueId; + var machineId = uniqueId; + + await Runner.WithContext(Context).RunScenarioAsync( + _ => _.Given_a_new_host(), + _ => Given_host_configuration_is_applied(), + _ => _.Given_a_Schematic_with_an_initial_state_INITIALSTATE(schematicName, "Initial"), + _ => _.Given_a_Machine_exists_with_MachineId_MACHINEID(_.CurrentSchematic, machineId), + _ => _.When_a_Machine_is_deleted_with_MachineId_MACHINEID(machineId), + _ => _.Then_no_exception_was_thrown(), + _ => _.When_a_Machine_is_retrieved_with_MachineId_MACHINEID(machineId), + _ => _.Then_MachineDoesNotExistException_is_thrown()); + } + } +} diff --git a/test/REstate.Tests/Features/Templates/REstateFeature.cs b/test/REstate.Tests/Features/Templates/REstateFeature.cs new file mode 100644 index 0000000..8235fd6 --- /dev/null +++ b/test/REstate.Tests/Features/Templates/REstateFeature.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using LightBDD.Framework; +using LightBDD.Framework.Scenarios.Contextual; +using LightBDD.XUnit2; +using REstate.Tests.Features.Context; + +// ReSharper disable InconsistentNaming + +namespace REstate.Tests.Features.Templates +{ + public class REstateFeature + : FeatureFixture + where TContext : REstateContext, new() + { + protected REstateFeature() + { + Context = new TContext(); + } + + protected virtual Task Given_host_configuration_is_applied() + { + return Task.FromResult( + CompositeStep + .DefineNew() + .WithContext(Context) + .Build()); + } + + protected TContext Context { get; } + } +} \ No newline at end of file diff --git a/test/REstate.Tests/REstate.Tests.csproj b/test/REstate.Tests/REstate.Tests.csproj index bea48f2..57750d6 100644 --- a/test/REstate.Tests/REstate.Tests.csproj +++ b/test/REstate.Tests/REstate.Tests.csproj @@ -15,9 +15,9 @@ - + - +