Make cycle detection throw an exception instead of returning null. (#4606)
* init * fix ci * throw a more specific exception type * fix error * fix * Resource explorer: bundle 'allow cycles' into ResourceExplorerOptions to prevent overload explosion due to binary compat requirements Co-authored-by: carlosscastro <ccastro@microsoft.com>
This commit is contained in:
Родитель
4677d4ec6c
Коммит
81be624cdd
|
@ -71,6 +71,10 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Converters
|
||||||
result = new DialogExpression((Dialog)converter.ReadJson(jsonTextReader, objectType, existingValue, serializer));
|
result = new DialogExpression((Dialog)converter.ReadJson(jsonTextReader, objectType, existingValue, serializer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
#pragma warning disable CA1031 // Do not catch general exception types (return an empty if an exception happens).
|
#pragma warning disable CA1031 // Do not catch general exception types (return an empty if an exception happens).
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
#pragma warning restore CA1031 // Do not catch general exception types
|
#pragma warning restore CA1031 // Do not catch general exception types
|
||||||
|
|
|
@ -19,8 +19,18 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Observers
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal class CycleDetectionObserver : IJsonLoadObserver
|
internal class CycleDetectionObserver : IJsonLoadObserver
|
||||||
{
|
{
|
||||||
|
private readonly bool allowCycle;
|
||||||
private readonly Dictionary<int, object> cache = new Dictionary<int, object>();
|
private readonly Dictionary<int, object> cache = new Dictionary<int, object>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CycleDetectionObserver"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="allowCycle">If allowCycle is set to false, throw an exception when detecting cycle.</param>
|
||||||
|
public CycleDetectionObserver(bool allowCycle = true)
|
||||||
|
{
|
||||||
|
this.allowCycle = allowCycle;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current pass of the algorithm.
|
/// Gets or sets the current pass of the algorithm.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -49,6 +59,11 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Observers
|
||||||
// If the same source range appears twice in the stack, we have a cycle.
|
// If the same source range appears twice in the stack, we have a cycle.
|
||||||
var isCycle = context.CallStack.Count(s => s.Equals(range)) > 1;
|
var isCycle = context.CallStack.Count(s => s.Equals(range)) > 1;
|
||||||
|
|
||||||
|
if (isCycle && !allowCycle)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Cycle detected for range: {range}");
|
||||||
|
}
|
||||||
|
|
||||||
if (isCycle && CycleDetectionPass == CycleDetectionPasses.PassOne)
|
if (isCycle && CycleDetectionPass == CycleDetectionPasses.PassOne)
|
||||||
{
|
{
|
||||||
// If we found a cycle, stop iterating and set the value to null in pass 1.
|
// If we found a cycle, stop iterating and set the value to null in pass 1.
|
||||||
|
|
|
@ -29,6 +29,8 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
private readonly ConcurrentDictionary<string, ICustomDeserializer> kindDeserializers = new ConcurrentDictionary<string, ICustomDeserializer>();
|
private readonly ConcurrentDictionary<string, ICustomDeserializer> kindDeserializers = new ConcurrentDictionary<string, ICustomDeserializer>();
|
||||||
private readonly ConcurrentDictionary<string, Type> kindToType = new ConcurrentDictionary<string, Type>();
|
private readonly ConcurrentDictionary<string, Type> kindToType = new ConcurrentDictionary<string, Type>();
|
||||||
private readonly ConcurrentDictionary<Type, List<string>> typeToKinds = new ConcurrentDictionary<Type, List<string>>();
|
private readonly ConcurrentDictionary<Type, List<string>> typeToKinds = new ConcurrentDictionary<Type, List<string>>();
|
||||||
|
private readonly ResourceExplorerOptions options;
|
||||||
|
|
||||||
private List<ResourceProvider> resourceProviders = new List<ResourceProvider>();
|
private List<ResourceProvider> resourceProviders = new List<ResourceProvider>();
|
||||||
private List<IComponentDeclarativeTypes> declarativeTypes;
|
private List<IComponentDeclarativeTypes> declarativeTypes;
|
||||||
private CancellationTokenSource cancelReloadToken = new CancellationTokenSource();
|
private CancellationTokenSource cancelReloadToken = new CancellationTokenSource();
|
||||||
|
@ -42,6 +44,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
/// Initializes a new instance of the <see cref="ResourceExplorer"/> class.
|
/// Initializes a new instance of the <see cref="ResourceExplorer"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ResourceExplorer()
|
public ResourceExplorer()
|
||||||
|
: this(new ResourceExplorerOptions())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +53,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="providers">The list of resource providers to initialize the current instance.</param>
|
/// <param name="providers">The list of resource providers to initialize the current instance.</param>
|
||||||
public ResourceExplorer(IEnumerable<ResourceProvider> providers)
|
public ResourceExplorer(IEnumerable<ResourceProvider> providers)
|
||||||
: this(providers, null)
|
: this(new ResourceExplorerOptions() { Providers = providers })
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,9 +63,27 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
/// <param name="providers">The list of resource providers to initialize the current instance.</param>
|
/// <param name="providers">The list of resource providers to initialize the current instance.</param>
|
||||||
/// <param name="declarativeTypes">A list of declarative types to use. Falls back to <see cref="ComponentRegistration.Components" /> if set to null.</param>
|
/// <param name="declarativeTypes">A list of declarative types to use. Falls back to <see cref="ComponentRegistration.Components" /> if set to null.</param>
|
||||||
public ResourceExplorer(IEnumerable<ResourceProvider> providers, IEnumerable<IComponentDeclarativeTypes> declarativeTypes)
|
public ResourceExplorer(IEnumerable<ResourceProvider> providers, IEnumerable<IComponentDeclarativeTypes> declarativeTypes)
|
||||||
|
: this(new ResourceExplorerOptions() { Providers = providers, DeclarativeTypes = declarativeTypes })
|
||||||
{
|
{
|
||||||
this.resourceProviders = providers.ToList();
|
}
|
||||||
this.declarativeTypes = declarativeTypes?.ToList();
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ResourceExplorer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Configuration optiosn for <see cref="ResourceExplorer"/>.</param>
|
||||||
|
public ResourceExplorer(ResourceExplorerOptions options)
|
||||||
|
{
|
||||||
|
this.options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
if (options.Providers != null)
|
||||||
|
{
|
||||||
|
this.resourceProviders = options.Providers.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.DeclarativeTypes != null)
|
||||||
|
{
|
||||||
|
this.declarativeTypes = options.DeclarativeTypes.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -188,6 +209,10 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception err)
|
catch (Exception err)
|
||||||
{
|
{
|
||||||
if (err.InnerException is SyntaxErrorException)
|
if (err.InnerException is SyntaxErrorException)
|
||||||
|
@ -589,7 +614,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cycle detection observer
|
// Create a cycle detection observer
|
||||||
var cycleDetector = new CycleDetectionObserver();
|
var cycleDetector = new CycleDetectionObserver(options.AllowCycles);
|
||||||
|
|
||||||
// Register our cycle detector on the converters that support observer registration
|
// Register our cycle detector on the converters that support observer registration
|
||||||
foreach (var observableConverter in converters.Where(c => c is IObservableJsonConverter))
|
foreach (var observableConverter in converters.Where(c => c is IObservableJsonConverter))
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.Bot.Builder.Dialogs.Declarative.Resources
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for <see cref="ResourceExplorer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ResourceExplorerOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether whether cycles are allowed in resources managed by the <see cref="ResourceExplorer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Whether cycles are allowed in resources managed by the <see cref="ResourceExplorer"/>.
|
||||||
|
/// </value>
|
||||||
|
public bool AllowCycles { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of resource providers to initialize the current the <see cref="ResourceExplorer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The list of resource providers to initialize the current the <see cref="ResourceExplorer"/>.
|
||||||
|
/// </value>
|
||||||
|
public IEnumerable<ResourceProvider> Providers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of declarative types to use. Falls back to <see cref="ComponentRegistration.Components" /> if set to null.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The list of declarative types to use. Falls back to <see cref="ComponentRegistration.Components" /> if set to null.
|
||||||
|
/// </value>
|
||||||
|
public IEnumerable<IComponentDeclarativeTypes> DeclarativeTypes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
// Licensed under the MIT License.
|
// Licensed under the MIT License.
|
||||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Bot.Builder.Adapters;
|
using Microsoft.Bot.Builder.Adapters;
|
||||||
|
@ -16,10 +17,12 @@ namespace Microsoft.Bot.Builder.Dialogs.Loader.Tests
|
||||||
public class JsonLoadTests : IClassFixture<ResourceExplorerFixture>
|
public class JsonLoadTests : IClassFixture<ResourceExplorerFixture>
|
||||||
{
|
{
|
||||||
private static ResourceExplorer _resourceExplorer;
|
private static ResourceExplorer _resourceExplorer;
|
||||||
|
private static ResourceExplorer _noCycleResourceExplorer;
|
||||||
|
|
||||||
public JsonLoadTests(ResourceExplorerFixture resourceExplorerFixture)
|
public JsonLoadTests(ResourceExplorerFixture resourceExplorerFixture)
|
||||||
{
|
{
|
||||||
_resourceExplorer = resourceExplorerFixture.ResourceExplorer;
|
_resourceExplorer = resourceExplorerFixture.ResourceExplorer;
|
||||||
|
_noCycleResourceExplorer = resourceExplorerFixture.NoCycleResourceExplorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -46,6 +49,12 @@ namespace Microsoft.Bot.Builder.Dialogs.Loader.Tests
|
||||||
.StartTestAsync();
|
.StartTestAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDialogLoad_CycleDetectionWithNoCycleMode()
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() => BuildNoCycleTestFlow(@"Root.dialog", nameof(JsonDialogLoad_CycleDetectionWithNoCycleMode)));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task JsonDialogLoad_Actions()
|
public async Task JsonDialogLoad_Actions()
|
||||||
{
|
{
|
||||||
|
@ -425,11 +434,12 @@ namespace Microsoft.Bot.Builder.Dialogs.Loader.Tests
|
||||||
return adapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TestFlow GetTestFlow(Dialog dialog, TestAdapter adapter)
|
private TestFlow GetTestFlow(Dialog dialog, TestAdapter adapter, bool allowCycle = true)
|
||||||
{
|
{
|
||||||
var dm = new DialogManager(dialog)
|
var dm = new DialogManager(dialog)
|
||||||
.UseResourceExplorer(_resourceExplorer)
|
.UseResourceExplorer(allowCycle ? _resourceExplorer : _noCycleResourceExplorer)
|
||||||
.UseLanguageGeneration();
|
.UseLanguageGeneration();
|
||||||
|
|
||||||
dm.InitialTurnState.Add<IQnAMakerClient>(new MockQnAMakerClient());
|
dm.InitialTurnState.Add<IQnAMakerClient>(new MockQnAMakerClient());
|
||||||
|
|
||||||
return new TestFlow(adapter, async (turnContext, cancellationToken) =>
|
return new TestFlow(adapter, async (turnContext, cancellationToken) =>
|
||||||
|
@ -444,5 +454,12 @@ namespace Microsoft.Bot.Builder.Dialogs.Loader.Tests
|
||||||
var dialog = _resourceExplorer.LoadType<Dialog>(resourceName);
|
var dialog = _resourceExplorer.LoadType<Dialog>(resourceName);
|
||||||
return GetTestFlow(dialog, adapter);
|
return GetTestFlow(dialog, adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TestFlow BuildNoCycleTestFlow(string resourceName, string testName, bool sendTrace = false)
|
||||||
|
{
|
||||||
|
var adapter = InitializeAdapter(testName, sendTrace);
|
||||||
|
var dialog = _noCycleResourceExplorer.LoadType<Dialog>(resourceName);
|
||||||
|
return GetTestFlow(dialog, adapter, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,14 @@ namespace Microsoft.Bot.Builder.Dialogs.Loader.Tests
|
||||||
public ResourceExplorerFixture()
|
public ResourceExplorerFixture()
|
||||||
{
|
{
|
||||||
ResourceExplorer = new ResourceExplorer();
|
ResourceExplorer = new ResourceExplorer();
|
||||||
|
NoCycleResourceExplorer = new ResourceExplorer(new ResourceExplorerOptions { AllowCycles = false });
|
||||||
Initialize();
|
Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceExplorer ResourceExplorer { get; set; }
|
public ResourceExplorer ResourceExplorer { get; set; }
|
||||||
|
|
||||||
|
public ResourceExplorer NoCycleResourceExplorer { get; set; }
|
||||||
|
|
||||||
public ResourceExplorerFixture Initialize()
|
public ResourceExplorerFixture Initialize()
|
||||||
{
|
{
|
||||||
var projPath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, PathUtils.NormalizePath($@"..\..\..\..\..\tests\Microsoft.Bot.Builder.TestBot.Json\Microsoft.Bot.Builder.TestBot.Json.csproj")));
|
var projPath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, PathUtils.NormalizePath($@"..\..\..\..\..\tests\Microsoft.Bot.Builder.TestBot.Json\Microsoft.Bot.Builder.TestBot.Json.csproj")));
|
||||||
|
@ -24,12 +27,16 @@ namespace Microsoft.Bot.Builder.Dialogs.Loader.Tests
|
||||||
ResourceExplorer = new ResourceExplorer()
|
ResourceExplorer = new ResourceExplorer()
|
||||||
.LoadProject(projPath, monitorChanges: false);
|
.LoadProject(projPath, monitorChanges: false);
|
||||||
|
|
||||||
|
NoCycleResourceExplorer = new ResourceExplorer(options: new ResourceExplorerOptions() { AllowCycles = false })
|
||||||
|
.LoadProject(projPath, monitorChanges: false);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
ResourceExplorer.Dispose();
|
ResourceExplorer.Dispose();
|
||||||
|
NoCycleResourceExplorer.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче