Added support for multiple recipe steps of the same type and added unit tests.

Enables execution of multiple child recipes in a particular sequence. This is useful for the RecipesStep for example, where you want to execute a child recipe first, then execute some commands and import some content, then execute a second child recipe.
This commit is contained in:
Sipke Schoorstra
2015-07-30 14:26:01 +01:00
parent a7d2777478
commit 73170a0d63
17 changed files with 147 additions and 69 deletions

View File

@@ -159,6 +159,7 @@
<Compile Include="Packaging\Services\FileBasedProjectSystemTests.cs" />
<Compile Include="Packaging\Services\FolderUpdaterTests.cs" />
<Compile Include="Packaging\Services\PackageInstallerTests.cs" />
<Compile Include="Recipes\RecipeHandlers\RecipeParserTest.cs" />
<Compile Include="Recipes\RecipeHandlers\RecipeExecutionStepHandlerTest.cs" />
<Compile Include="Recipes\RecipeHandlers\ModuleStepTest.cs" />
<Compile Include="Recipes\RecipeHandlers\ThemeStepTest.cs" />
@@ -336,6 +337,12 @@
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Recipes\Services\FoldersData\Sample2\Module.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Recipes\Services\FoldersData\Sample2\Recipes\duplicate-steps.recipe.xml" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -94,7 +94,7 @@ Features:
Enumerable.Empty<ShellParameter>());
var moduleStep = _container.Resolve<ModuleStep>();
var recipeExecutionContext = new RecipeExecutionContext {RecipeStep = new RecipeStep( recipeName: "Test", name: "Module", step: new XElement("SuperWiki")) };
var recipeExecutionContext = new RecipeExecutionContext {RecipeStep = new RecipeStep(id: "1", recipeName: "Test", name: "Module", step: new XElement("SuperWiki")) };
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("packageId", "Orchard.Module.SuperWiki"));
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("repository", "test"));
@@ -120,7 +120,7 @@ Features:
");
var moduleStep = _container.Resolve<ModuleStep>();
var recipeContext = new RecipeContext { RecipeStep = new RecipeStep(recipeName: "Test", name: "Module", step: new XElement("SuperWiki")) };
var recipeContext = new RecipeContext { RecipeStep = new RecipeStep(id: "1", recipeName: "Test", name: "Module", step: new XElement("SuperWiki")) };
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = recipeContext.RecipeStep };
recipeContext.RecipeStep.Step.Add(new XAttribute("repository", "test"));
@@ -145,7 +145,7 @@ Features:
});
var moduleStep = _container.Resolve<ModuleStep>();
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = new RecipeStep(recipeName: "Test", name: "Module", step: new XElement("SuperWiki")) };
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = new RecipeStep(id: "1", recipeName: "Test", name: "Module", step: new XElement("SuperWiki")) };
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("packageId", "Orchard.Module.SuperWiki"));
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("repository", "test"));

View File

@@ -25,7 +25,7 @@ namespace Orchard.Tests.Modules.Recipes.RecipeHandlers {
var fakeRecipeStep = _container.Resolve<StubRecipeExecutionStep>();
var context = new RecipeContext {
RecipeStep = new RecipeStep (recipeName: "FakeRecipe", name: "FakeRecipeStep", step: new XElement("FakeRecipeStep")),
RecipeStep = new RecipeStep (id: "1", recipeName: "FakeRecipe", name: "FakeRecipeStep", step: new XElement("FakeRecipeStep")),
ExecutionId = "12345"
};

View File

@@ -0,0 +1,29 @@
using System.Linq;
using Autofac;
using NUnit.Framework;
using Orchard.Recipes.Services;
namespace Orchard.Tests.Modules.Recipes.RecipeHandlers {
[TestFixture]
public class RecipeParserTest {
protected IContainer _container;
[SetUp]
public void Init() {
var builder = new ContainerBuilder();
builder.RegisterType<RecipeParser>().As<IRecipeParser>();
_container = builder.Build();
}
[Test]
public void ParsingRecipeYieldsUniqueIdsForSteps() {
var recipeText = @"<Orchard><Foo /><Bar /><Baz /></Orchard>";
var recipeParser = _container.Resolve<IRecipeParser>();
var recipe = recipeParser.ParseRecipe(recipeText);
// Assert that each step has a unique ID.
Assert.IsTrue(recipe.RecipeSteps.GroupBy(x => x.Id).All(y => y.Count() == 1));
}
}
}

View File

@@ -100,7 +100,7 @@ Features:
Enumerable.Empty<ShellParameter>());
var themeStep = _container.Resolve<ThemeStep>();
var recipeExecutionContext = new RecipeExecutionContext {RecipeStep = new RecipeStep (recipeName: "Test", name: "Theme", step: new XElement("SuperWiki")) };
var recipeExecutionContext = new RecipeExecutionContext {RecipeStep = new RecipeStep (id: "1", recipeName: "Test", name: "Theme", step: new XElement("SuperWiki")) };
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("packageId", "Orchard.Theme.SuperWiki"));
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("repository", "test"));
@@ -135,7 +135,7 @@ Features:
");
var themeStep = _container.Resolve<ThemeStep>();
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = new RecipeStep(recipeName: "Test", name: "Theme", step: new XElement("SuperWiki")) };
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = new RecipeStep(id: "1", recipeName: "Test", name: "Theme", step: new XElement("SuperWiki")) };
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("repository", "test"));
Assert.Throws(typeof (InvalidOperationException), () => themeStep.Execute(recipeExecutionContext));
@@ -159,7 +159,7 @@ Features:
});
var themeStep = _container.Resolve<ThemeStep>();
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = new RecipeStep(recipeName: "Test", name: "Theme", step: new XElement("SuperWiki")) };
var recipeExecutionContext = new RecipeExecutionContext { RecipeStep = new RecipeStep(id: "1", recipeName: "Test", name: "Theme", step: new XElement("SuperWiki")) };
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("packageId", "Orchard.Theme.SuperWiki"));
recipeExecutionContext.RecipeStep.Step.Add(new XAttribute("repository", "test"));

View File

@@ -0,0 +1,8 @@
Name: Le plug-in français
Author: Bertrand Le Roy
Description:
This plug-in replaces
'the' with 'le'
Version: 1.4.1
Tags: plug-in, français, the, le
homepage: http://weblogs.asp.net/bleroy

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<Orchard>
<Recipe>
<Name>Duplicate Steps</Name>
</Recipe>
<Recipes />
<Recipes />
<Recipes />
</Orchard>

View File

@@ -1,61 +1,52 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Xml;
using Autofac;
using Moq;
using NHibernate;
using NUnit.Framework;
using Orchard.Caching;
using Orchard.ContentManagement.Records;
using Orchard.Data;
using Orchard.Environment.Extensions;
using Orchard.Environment.Extensions.Folders;
using Orchard.Environment.Extensions.Loaders;
using Orchard.FileSystems.AppData;
using Orchard.FileSystems.WebSite;
using Orchard.Recipes.Events;
using Orchard.Recipes.Models;
using Orchard.Recipes.Services;
using Orchard.Services;
using Orchard.Tests.Environment.Extensions;
using Orchard.Tests.Stubs;
using Orchard.Recipes.Events;
using System;
using System.Linq.Expressions;
namespace Orchard.Tests.Modules.Recipes.Services {
[TestFixture]
public class RecipeManagerTests {
private IContainer _container;
public class RecipeManagerTests : DatabaseEnabledTestsBase {
private IRecipeManager _recipeManager;
private IRecipeHarvester _recipeHarvester;
private IRecipeParser _recipeParser;
private IExtensionFolders _folders;
private ISessionFactory _sessionFactory;
private ISession _session;
private const string DataPrefix = "Orchard.Tests.Modules.Recipes.Services.FoldersData.";
private string _tempFolderName;
[TestFixtureSetUp]
public void InitFixture() {
var databaseFileName = System.IO.Path.GetTempFileName();
_sessionFactory = DataUtility.CreateSessionFactory(
databaseFileName,
typeof(ContentTypeRecord),
typeof(ContentItemRecord),
typeof(ContentItemVersionRecord),
typeof(RecipeStepResultRecord));
protected override IEnumerable<Type> DatabaseTypes {
get { yield return typeof (RecipeStepResultRecord); }
}
[SetUp]
public void Init() {
public override void Register(ContainerBuilder builder) {
_tempFolderName = Path.GetTempFileName();
File.Delete(_tempFolderName);
var assembly = GetType().Assembly;
foreach (var name in assembly.GetManifestResourceNames()) {
if (name.StartsWith(DataPrefix)) {
foreach (var name in assembly.GetManifestResourceNames())
{
if (name.StartsWith(DataPrefix))
{
string text;
using (var stream = assembly.GetManifestResourceStream(name)) {
using (var stream = assembly.GetManifestResourceStream(name))
{
using (var reader = new StreamReader(stream))
text = reader.ReadToEnd();
@@ -73,15 +64,16 @@ namespace Orchard.Tests.Modules.Recipes.Services {
var targetPath = Path.Combine(_tempFolderName, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
using (var stream = new FileStream(targetPath, FileMode.Create)) {
using (var writer = new StreamWriter(stream)) {
using (var stream = new FileStream(targetPath, FileMode.Create))
{
using (var writer = new StreamWriter(stream))
{
writer.Write(text);
}
}
}
}
var builder = new ContainerBuilder();
var harvester = new ExtensionHarvester(new StubCacheManager(), new StubWebSiteFolder(), new Mock<ICriticalErrorProvider>().Object);
_folders = new ModuleFolders(new[] { _tempFolderName }, harvester);
builder.RegisterType<RecipeManager>().As<IRecipeManager>();
@@ -97,21 +89,23 @@ namespace Orchard.Tests.Modules.Recipes.Services {
builder.RegisterType<StubAsyncTokenProvider>().As<IAsyncTokenProvider>();
builder.RegisterInstance(_folders).As<IExtensionFolders>();
builder.RegisterInstance(new Mock<IRecipeExecuteEventHandler>().Object);
builder.RegisterType<Environment.Extensions.ExtensionManagerTests.StubLoaders>().As<IExtensionLoader>();
builder.RegisterType<ExtensionManagerTests.StubLoaders>().As<IExtensionLoader>();
builder.RegisterType<RecipeParser>().As<IRecipeParser>();
builder.RegisterType<StubWebSiteFolder>().As<IWebSiteFolder>();
builder.RegisterType<CustomRecipeHandler>().As<IRecipeHandler>();
builder.RegisterInstance(new StubRecipeStepResultRecordRepository()).As<IRepository<RecipeStepResultRecord>>();
}
public override void Init() {
base.Init();
_container = builder.Build();
_recipeManager = _container.Resolve<IRecipeManager>();
_recipeParser = _container.Resolve<IRecipeParser>();
_recipeHarvester = _container.Resolve<IRecipeHarvester>();
}
[TearDown]
public void Term() {
public override void Cleanup() {
Directory.Delete(_tempFolderName, true);
base.Cleanup();
}
[Test]
@@ -168,6 +162,38 @@ namespace Orchard.Tests.Modules.Recipes.Services {
Assert.That(CustomRecipeHandler.AttributeValue == "value1");
}
[Test]
public void ExecuteUpdatesStepResults()
{
var recipes = (List<Recipe>)_recipeHarvester.HarvestRecipes("Sample1");
var sampleRecipe = recipes.First();
var steps = sampleRecipe.RecipeSteps.ToArray();
_recipeManager.Execute(sampleRecipe);
var stepResultRepository = _container.Resolve<IRepository<RecipeStepResultRecord>>();
var stepResults = stepResultRepository.Table.ToArray();
Assert.That(stepResults.Count(), Is.EqualTo(steps.Count()));
Assert.IsTrue(stepResults.All(x => x.IsCompleted));
}
[Test]
public void CanExecuteSameStepMultipleTimes()
{
var recipes = (List<Recipe>)_recipeHarvester.HarvestRecipes("Sample2");
var recipe = recipes.Single(x => x.Name == "Duplicate Steps");
var steps = recipe.RecipeSteps.ToArray();
_recipeManager.Execute(recipe);
var stepResultRepository = _container.Resolve<IRepository<RecipeStepResultRecord>>();
var stepResults = stepResultRepository.Table.ToArray();
Assert.That(stepResults.Count(), Is.EqualTo(steps.Count()));
Assert.IsTrue(stepResults.All(x => x.IsCompleted));
}
}
public class StubStepQueue : IRecipeStepQueue {
@@ -182,22 +208,6 @@ namespace Orchard.Tests.Modules.Recipes.Services {
}
}
public class StubRecipeStepResultRecordRepository : IRepository<RecipeStepResultRecord> {
private List<RecipeStepResultRecord> _records = new List<RecipeStepResultRecord>();
public IQueryable<RecipeStepResultRecord> Table { get { return _records.AsQueryable(); } }
public void Copy(RecipeStepResultRecord source, RecipeStepResultRecord target) { }
public int Count(Expression<Func<RecipeStepResultRecord, bool>> predicate) { return _records.Count; }
public void Create(RecipeStepResultRecord entity) { _records.Add(entity); }
public void Delete(RecipeStepResultRecord entity) { _records.Remove(entity); }
public IEnumerable<RecipeStepResultRecord> Fetch(Expression<Func<RecipeStepResultRecord, bool>> predicate) { throw new NotImplementedException(); }
public IEnumerable<RecipeStepResultRecord> Fetch(Expression<Func<RecipeStepResultRecord, bool>> predicate, Action<Orderable<RecipeStepResultRecord>> order) { throw new NotImplementedException(); }
public IEnumerable<RecipeStepResultRecord> Fetch(Expression<Func<RecipeStepResultRecord, bool>> predicate, Action<Orderable<RecipeStepResultRecord>> order, int skip, int count) { throw new NotImplementedException(); }
public void Flush() { }
public RecipeStepResultRecord Get(Expression<Func<RecipeStepResultRecord, bool>> predicate) { throw new NotImplementedException(); }
public RecipeStepResultRecord Get(int id) { throw new NotImplementedException(); }
public void Update(RecipeStepResultRecord entity) { }
}
public class StubRecipeScheduler : IRecipeScheduler {
private readonly IRecipeStepExecutor _recipeStepExecutor;
@@ -212,7 +222,7 @@ namespace Orchard.Tests.Modules.Recipes.Services {
public class CustomRecipeHandler : IRecipeHandler {
public static string AttributeValue;
public string[] _handles = { "Module", "Theme", "Migration", "Custom1", "Custom2", "Command", "Metadata", "Feature", "Settings" };
public string[] _handles = { "Module", "Theme", "Migration", "Custom1", "Custom2", "Command", "Metadata", "Feature", "Settings", "Recipes" };
public void ExecuteRecipeStep(RecipeContext recipeContext) {
if (_handles.Contains(recipeContext.RecipeStep.Name)) {

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -255,7 +256,8 @@ namespace Orchard.Packaging.Controllers {
var recipe = new Recipe {
Name = "Test",
RecipeSteps = featureIds.Select(
x => new RecipeStep(
(i,x) => new RecipeStep(
id: i.ToString(CultureInfo.InvariantCulture),
recipeName: "Test",
name: "Feature",
step: new XElement("Feature", new XAttribute("enable", x))))

View File

@@ -7,6 +7,7 @@ namespace Orchard.Recipes {
.Column<int>("Id", c => c.PrimaryKey().Identity())
.Column<string>("ExecutionId", c => c.WithLength(128).NotNull())
.Column<string>("RecipeName", c => c.WithLength(256))
.Column<string>("StepId", c => c.WithLength(32).NotNull())
.Column<string>("StepName", c => c.WithLength(256).NotNull())
.Column<bool>("IsCompleted", c => c.NotNull())
.Column<bool>("IsSuccessful", c => c.NotNull())

View File

@@ -3,6 +3,7 @@
public virtual int Id { get; set; }
public virtual string ExecutionId { get; set; }
public virtual string RecipeName { get; set; }
public virtual string StepId { get; set; }
public virtual string StepName { get; set; }
public virtual bool IsCompleted { get; set; }
public virtual bool IsSuccessful { get; set; }

View File

@@ -65,6 +65,7 @@ namespace Orchard.Recipes.Providers.Executors {
_recipeStepResultRecordRepository.Create(new RecipeStepResultRecord {
ExecutionId = executionId,
RecipeName = recipe.Name,
StepId = recipeStep.Id,
StepName = recipeStep.Name
});
}

View File

@@ -48,6 +48,7 @@ namespace Orchard.Recipes.Services {
_recipeStepResultRecordRepository.Create(new RecipeStepResultRecord {
ExecutionId = executionId,
RecipeName = recipe.Name,
StepId = recipeStep.Id,
StepName = recipeStep.Name
});
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using System.Xml.Linq;
using Orchard.Logging;
@@ -11,6 +12,7 @@ namespace Orchard.Recipes.Services {
public Recipe ParseRecipe(XDocument recipeDocument) {
var recipe = new Recipe();
var recipeSteps = new List<RecipeStep>();
var stepId = 0;
foreach (var element in recipeDocument.Root.Elements()) {
// Recipe metadata.
@@ -52,7 +54,7 @@ namespace Orchard.Recipes.Services {
}
// Recipe step.
else {
var recipeStep = new RecipeStep(recipeName: recipe.Name, name: element.Name.LocalName, step: element );
var recipeStep = new RecipeStep(id: (++stepId).ToString(CultureInfo.InvariantCulture), recipeName: recipe.Name, name: element.Name.LocalName, step: element );
recipeSteps.Add(recipeStep);
}
}

View File

@@ -11,18 +11,18 @@ namespace Orchard.Recipes.Services {
private readonly IRecipeStepQueue _recipeStepQueue;
private readonly IEnumerable<IRecipeHandler> _recipeHandlers;
private readonly IRecipeExecuteEventHandler _recipeExecuteEventHandler;
private readonly IRepository<RecipeStepResultRecord> _recipeStepResultRecordRepository;
private readonly IRepository<RecipeStepResultRecord> _recipeStepResultRepository;
public RecipeStepExecutor(
IRecipeStepQueue recipeStepQueue,
IEnumerable<IRecipeHandler> recipeHandlers,
IRecipeExecuteEventHandler recipeExecuteEventHandler,
IRepository<RecipeStepResultRecord> recipeStepResultRecordRepository) {
IRepository<RecipeStepResultRecord> recipeStepResultRepository) {
_recipeStepQueue = recipeStepQueue;
_recipeHandlers = recipeHandlers;
_recipeExecuteEventHandler = recipeExecuteEventHandler;
_recipeStepResultRecordRepository = recipeStepResultRecordRepository;
_recipeStepResultRepository = recipeStepResultRepository;
}
public bool ExecuteNextStep(string executionId) {
@@ -44,11 +44,11 @@ namespace Orchard.Recipes.Services {
recipeHandler.ExecuteRecipeStep(recipeContext);
}
UpdateStepResultRecord(executionId, nextRecipeStep.RecipeName, nextRecipeStep.Name, isSuccessful: true);
UpdateStepResultRecord(executionId, nextRecipeStep.RecipeName, nextRecipeStep.Id, nextRecipeStep.Name, isSuccessful: true);
_recipeExecuteEventHandler.RecipeStepExecuted(executionId, recipeContext);
}
catch (Exception ex) {
UpdateStepResultRecord(executionId, nextRecipeStep.RecipeName, nextRecipeStep.Name, isSuccessful: false, errorMessage: ex.Message);
UpdateStepResultRecord(executionId, nextRecipeStep.RecipeName, nextRecipeStep.Id, nextRecipeStep.Name, isSuccessful: false, errorMessage: ex.Message);
Logger.Error(ex, "Recipe execution failed because the step '{0}' failed.", nextRecipeStep.Name);
while (_recipeStepQueue.Dequeue(executionId) != null);
var message = T("Recipe execution with ID {0} failed because the step '{1}' failed to execute. The following exception was thrown:\n{2}\nRefer to the error logs for more information.", executionId, nextRecipeStep.Name, ex.Message);
@@ -65,10 +65,10 @@ namespace Orchard.Recipes.Services {
return true;
}
private void UpdateStepResultRecord(string executionId, string recipeName, string stepName, bool isSuccessful, string errorMessage = null) {
private void UpdateStepResultRecord(string executionId, string recipeName, string stepId, string stepName, bool isSuccessful, string errorMessage = null) {
var query =
from record in _recipeStepResultRecordRepository.Table
where record.ExecutionId == executionId && record.StepName == stepName
from record in _recipeStepResultRepository.Table
where record.ExecutionId == executionId && record.StepId == stepId && record.StepName == stepName
select record;
if (!String.IsNullOrWhiteSpace(recipeName))
@@ -80,7 +80,7 @@ namespace Orchard.Recipes.Services {
stepResultRecord.IsSuccessful = isSuccessful;
stepResultRecord.ErrorMessage = errorMessage;
_recipeStepResultRecordRepository.Update(stepResultRecord);
_recipeStepResultRepository.Update(stepResultRecord);
}
}
}

View File

@@ -26,6 +26,7 @@ namespace Orchard.Recipes.Services {
public void Enqueue(string executionId, RecipeStep step) {
Logger.Information("Enqueuing recipe step '{0}'.", step.Name);
var recipeStepElement = new XElement("RecipeStep");
recipeStepElement.Attr("Id", step.Id);
recipeStepElement.Attr("RecipeName", step.RecipeName);
recipeStepElement.Add(new XElement("Name", step.Name));
recipeStepElement.Add(step.Step);
@@ -54,9 +55,10 @@ namespace Orchard.Recipes.Services {
// string to xelement
var stepElement = XElement.Parse(_appDataFolder.ReadFile(stepPath));
var stepName = stepElement.Element("Name").Value;
var stepId = stepElement.Attr("Id");
var recipeName = stepElement.Attr("RecipeName");
Logger.Information("Dequeuing recipe step '{0}'.", stepName);
recipeStep = new RecipeStep(recipeName: recipeName, name: stepName, step: stepElement.Element(stepName));
recipeStep = new RecipeStep(id: stepId, recipeName: recipeName, name: stepName, step: stepElement.Element(stepName));
_appDataFolder.DeleteFile(stepPath);
}

View File

@@ -2,12 +2,14 @@
namespace Orchard.Recipes.Models {
public class RecipeStep {
public RecipeStep(string recipeName, string name, XElement step) {
public RecipeStep(string id, string recipeName, string name, XElement step) {
Id = id;
RecipeName = recipeName;
Name = name;
Step = step;
}
public string Id { get; set; }
public string RecipeName { get; private set; }
public string Name { get; private set; }
public XElement Step { get; private set; }