diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 68402de85f..49edf6cc05 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -4,6 +4,7 @@ using System.Linq; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; +using Type = System.Type; namespace Umbraco.Core.Migrations { @@ -31,6 +32,11 @@ namespace Umbraco.Core.Migrations DefinePlan(); } + /// + /// Gets the transitions. + /// + public IReadOnlyDictionary Transitions => _transitions; + /// /// Defines the plan. /// @@ -238,8 +244,8 @@ namespace Umbraco.Core.Migrations { Validate(); - if (migrationBuilder == null || logger == null) - throw new InvalidOperationException("Cannot execute a non-executable plan."); + if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); logger.Info("Starting '{MigrationName}'...", Name); @@ -275,10 +281,47 @@ namespace Umbraco.Core.Migrations return origState; } + /// + /// Follows a path (for tests and debugging). + /// + /// Does the same thing Execute does, but does not actually execute migrations. + internal string FollowPath(string fromState, string toState = null) + { + toState = toState.NullOrWhiteSpaceAsNull(); + + Validate(); + + var origState = fromState ?? string.Empty; + + if (!_transitions.TryGetValue(origState, out var transition)) + throw new Exception($"Unknown state \"{origState}\"."); + + while (transition != null) + { + var nextState = transition.TargetState; + origState = nextState; + + if (nextState == toState) + { + transition = null; + continue; + } + + if (!_transitions.TryGetValue(origState, out transition)) + throw new Exception($"Unknown state \"{origState}\"."); + } + + // safety check + if (origState != (toState ?? _finalState)) + throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); + + return origState; + } + /// /// Represents a plan transition. /// - private class Transition + public class Transition { /// /// Initializes a new instance of the class. diff --git a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs index 4b966a5d80..3795ed79af 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs @@ -41,6 +41,11 @@ namespace Umbraco.Core.Migrations.Upgrade /// A logger. public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger) { + if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); + if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); + if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + var plan = Plan; using (var scope = scopeProvider.CreateScope()) diff --git a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs index d6ac7abff4..9aec42c252 100644 --- a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs +++ b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Upgrade; @@ -141,6 +143,40 @@ namespace Umbraco.Tests.Migrations Assert.IsFalse(string.IsNullOrWhiteSpace(plan.FinalState)); } + [Test] + public void CanCopyChain() + { + var plan = new MigrationPlan("default"); + plan + .From(string.Empty) + .To("aaa") + .To("bbb") + .To("ccc") + .To("ddd") + .To("eee"); + + plan + .From("xxx") + .To("yyy", "bbb", "ddd") + .To("eee"); + + WritePlanToConsole(plan); + + plan.Validate(); + Assert.AreEqual("eee", plan.FollowPath("xxx")); + Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy")); + } + + private void WritePlanToConsole(MigrationPlan plan) + { + var final = plan.Transitions.First(x => x.Value == null).Key; + + Console.WriteLine("plan \"{0}\" to final state \"{1}\":", plan.Name, final); + foreach (var (_, transition) in plan.Transitions) + if (transition != null) + Console.WriteLine(transition); + } + public class DeleteRedirectUrlTable : MigrationBase { public DeleteRedirectUrlTable(IMigrationContext context)