зеркало из https://github.com/microsoft/Forge.git
First commit - Adding all files.
This commit is contained in:
Коммит
57bd920101
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{C49A8494-13E5-4214-8434-708BA280C5B1}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Forge.DataContracts</RootNamespace>
|
||||
<AssemblyName>Forge.DataContracts</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ForgeTree.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="ForgeSchemaValidationRules.json" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -0,0 +1,193 @@
|
|||
{
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"TreeDefinition": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*?": {
|
||||
"$ref": "#/definitions/TreeNodeDefinition"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TreeNodeDefinition": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SelectionTypeNodeDefinition"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ActionTypeNodeDefinition"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/LeafTypeNodeDefinition"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SelectionTypeNodeDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string",
|
||||
"enum": [ "Selection" ]
|
||||
},
|
||||
"ChildSelector": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ChildSelectorDefinition"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"Properties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "Type" ]
|
||||
},
|
||||
"ActionTypeNodeDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string",
|
||||
"enum": [ "Action" ]
|
||||
},
|
||||
"Timeout": {
|
||||
"type": [ "number", "string" ]
|
||||
},
|
||||
"Actions": {
|
||||
"patternProperties": {
|
||||
".*?": {
|
||||
"$ref": "#/definitions/ActionDefinition"
|
||||
}
|
||||
},
|
||||
"minProperties": 1
|
||||
},
|
||||
"ChildSelector": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ChildSelectorDefinition"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"Properties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "Type", "Actions" ]
|
||||
},
|
||||
"LeafTypeNodeDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string",
|
||||
"enum": [ "Leaf" ]
|
||||
},
|
||||
"Actions": {
|
||||
"patternProperties": {
|
||||
".*?": {
|
||||
"$ref": "#/definitions/LeafNodeSummaryActionDefinition"
|
||||
}
|
||||
},
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1
|
||||
},
|
||||
"Properties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "Type" ]
|
||||
},
|
||||
"ChildSelectorDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Label": {
|
||||
"type": "string"
|
||||
},
|
||||
"ShouldSelect": {
|
||||
"type": "string"
|
||||
},
|
||||
"Child": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "Label", "Child" ]
|
||||
},
|
||||
"RetryPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string",
|
||||
"enum": [ "None", "FixedInterval", "ExponentialBackoff" ]
|
||||
},
|
||||
"MinBackoffMs": {
|
||||
"type": "number"
|
||||
},
|
||||
"MaxBackoffMs": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [ "Type" ]
|
||||
},
|
||||
"ActionDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Action": {
|
||||
"type": "string"
|
||||
},
|
||||
"Input": {
|
||||
"type": "object"
|
||||
},
|
||||
"Properties": {
|
||||
"type": "object"
|
||||
},
|
||||
"Timeout": {
|
||||
"type": [ "number", "string" ]
|
||||
},
|
||||
"ContinuationOnTimeout": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"RetryPolicy": {
|
||||
"$ref": "#/definitions/RetryPolicy"
|
||||
},
|
||||
"ContinuationOnRetryExhaustion": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "Action" ]
|
||||
},
|
||||
"LeafNodeSummaryActionDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Action": {
|
||||
"enum": [ "LeafNodeSummaryAction" ]
|
||||
},
|
||||
"Input": {
|
||||
"type": [ "object", "string" ],
|
||||
"properties": {
|
||||
"Status": {
|
||||
"type": "string"
|
||||
},
|
||||
"StatusCode": {
|
||||
"type": [ "number", "string" ]
|
||||
},
|
||||
"Output": {
|
||||
"type": [ "object", "string" ]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "Action", "Input" ]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"Tree": {
|
||||
"$ref": "#/definitions/TreeDefinition"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ForgeTree.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The Forge schema data contracts.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.DataContracts
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// The Forge tree.
|
||||
/// This outermost data structure holds the Forge schema.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ForgeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Dictionary mapping unique TreeNodeKeys to TreeNodes.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public Dictionary<string, TreeNode> Tree { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tree node.
|
||||
/// Holds information to navigate the tree and perform actions.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class TreeNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The tree node type.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public TreeNodeType Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional properties passed to wrapper class.
|
||||
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public dynamic Properties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The child selectors.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public ChildSelector[] ChildSelector { get; private set; }
|
||||
|
||||
#region Properties used only by TreeNodeType.Action nodes
|
||||
|
||||
/// <summary>
|
||||
/// The actions to execute when the TreeNodeType is Action.
|
||||
/// Dictionary mapping unique TreeActionKeys to TreeActions.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public Dictionary<string, TreeAction> Actions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in milliseconds for executing the TreeActions. Default to -1 (infinite) if not specified.
|
||||
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public dynamic Timeout { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The child selector for the TreeNode.
|
||||
/// Used to navigate the tree by referencing child TreeNodes.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ChildSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// String code-snippet that can be parsed and evaluated to a boolean value.
|
||||
/// If the expression is true, visit the attached child TreeNode.
|
||||
/// If the expression is empty, evaluate to true by default.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public string ShouldSelect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reader-friendly label that describes the intention of the ShouldSelect expression.
|
||||
/// Used in ForgeEditor for display purposes.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// String key pointer to a child TreeNode.
|
||||
/// Visit this child if the attached ShouldSelect expression evaluates to true.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public string Child { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tree action for the TreeNode.
|
||||
/// Holds instructions and policies for executing an action.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class TreeAction
|
||||
{
|
||||
/// <summary>
|
||||
/// String name of the action that maps to an action-task.
|
||||
/// These actions may be predefined Forge actions or action-tasks passed by a Wrapper class.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic input parameters passed to the action-task.
|
||||
/// Wrapper class is responsible for making sure the action-task input matches the input defined in the schema.
|
||||
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public dynamic Input { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional properties passed to wrapper class.
|
||||
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public dynamic Properties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in milliseconds for executing the action. Default to -1 (infinite) if not specified.
|
||||
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public dynamic Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A flag that represents how to handle the exit of the action due to timeout. If false (default), then the session will end on the
|
||||
/// timeout. If true and a timeout is hit, the action will continue on as if it were successful after committing a "TimeoutOnAction" response.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public bool ContinuationOnTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retry policy of the action.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public RetryPolicy RetryPolicy { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A flag that represents how to handle the exit of the action due to retry exhaustion. If false (default), then the session will end once
|
||||
/// retries are exhausted or no retries are specified. If true and retries are exhausted, the action will continue on as if it were successful
|
||||
/// after committing a "RetryExhaustedOnAction" response.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public bool ContinuationOnRetryExhaustion { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The retry policy for the TreeAction.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class RetryPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The retry policy type.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public RetryPolicyType Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum backoff time in milliseconds.
|
||||
/// When retrying an action, wait at least this long before your next attempt.
|
||||
/// This is useful to ensure actions are not retried too quickly.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public long MinBackoffMs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum backoff time in milliseconds.
|
||||
/// When retrying an action, wait at most this long before your next attempt.
|
||||
/// This is useful to ensure exponential backoff doesn't wait too long.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public long MaxBackoffMs { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The retry policy types.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public enum RetryPolicyType
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not retry.
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Retry at a fixed interval every MinBackoffMs.
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
FixedInterval = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Retry with an exponential backoff.
|
||||
/// Start with MinBackoffMs, then wait Math.Min(MinBackoffMs * 2^(retryCount), MaxBackoffMs).
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
ExponentialBackoff = 2
|
||||
|
||||
// TODO: Add a FixedCount type that will give the full timeout duration for the set number of retries.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tree node types.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public enum TreeNodeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Undefined.
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Selection type node.
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
Selection = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Action type node.
|
||||
/// This node includes TreeAction(s).
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
Action = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Leaf type node.
|
||||
/// This represents an end state in tree.
|
||||
/// </summary>
|
||||
[EnumMember]
|
||||
Leaf = 3
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Forge.DataContracts")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Forge.DataContracts")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("c49a8494-13e5-4214-8434-708ba280c5b1")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{A33AF1FF-1291-4CB1-A733-3243E1FE967E}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Forge.TreeWalker.UnitTests</RootNamespace>
|
||||
<AssemblyName>Forge.TreeWalker.UnitTests</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
|
||||
<IsCodedUITest>False</IsCodedUITest>
|
||||
<TestProjectType>UnitTest</TestProjectType>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="test\ExecutorUnitTests.cs" />
|
||||
<Compile Include="test\ExternalTestType.cs" />
|
||||
<Compile Include="test\ForgeSchemaHelper.cs" />
|
||||
<Compile Include="test\TreeWalkerUnitTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Forge.DataContracts\Forge.DataContracts.csproj">
|
||||
<Project>{c49a8494-13e5-4214-8434-708ba280c5b1}</Project>
|
||||
<Name>Forge.DataContracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Forge.TreeWalker\Forge.TreeWalker.csproj">
|
||||
<Project>{00fc1c22-6ae9-4f60-8a3e-05885ba34c9c}</Project>
|
||||
<Name>Forge.TreeWalker</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props'))" />
|
||||
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" />
|
||||
</Project>
|
|
@ -0,0 +1,20 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("Forge.TreeWalker.UnitTests")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Forge.TreeWalker.UnitTests")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
[assembly: Guid("a33af1ff-1291-4cb1-a733-3243e1fe967e")]
|
||||
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.2.3.0" newVersion="1.2.3.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="MSTest.TestAdapter" version="1.3.2" targetFramework="net462" />
|
||||
<package id="MSTest.TestFramework" version="1.3.2" targetFramework="net462" />
|
||||
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net462" />
|
||||
</packages>
|
|
@ -0,0 +1,311 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ExecutorUnitTests.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// Tests the ExpressionExecutor class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker.UnitTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Forge.ExternalTypes;
|
||||
using Forge.TreeWalker;
|
||||
|
||||
[TestClass]
|
||||
public class ExecutorUnitTests
|
||||
{
|
||||
private dynamic UserContext;
|
||||
|
||||
[TestInitialize]
|
||||
public void TestInitialize()
|
||||
{
|
||||
this.UserContext = new System.Dynamic.ExpandoObject();
|
||||
this.UserContext.GetTopic = (Func<string, dynamic>)((topicName) =>
|
||||
{
|
||||
dynamic result = new System.Dynamic.ExpandoObject();
|
||||
result.ResourceType = "Node";
|
||||
return result;
|
||||
});
|
||||
|
||||
this.UserContext.GetCount = (Func<int>)(() =>
|
||||
{
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_bool()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo == \"Bar\"";
|
||||
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_long()
|
||||
{
|
||||
// Note that long requires casting where int does not.
|
||||
this.UserContext.Foo = (long)1000;
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo";
|
||||
|
||||
Assert.AreEqual((long)1000, ex.Execute<long>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate the expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_int()
|
||||
{
|
||||
this.UserContext.Foo = 1000;
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo";
|
||||
|
||||
Assert.AreEqual(1000, ex.Execute<int>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate the expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_ExecuteTwice()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo == \"Bar\" && UserContext.GetTopic(\"TopicName\").ResourceType == \"Node\"";
|
||||
|
||||
// Test - confirm Execute can compile and execute the same code twice without crashing.
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Fail_MissingDefinitions()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Bar == \"Bar\"";
|
||||
|
||||
try
|
||||
{
|
||||
ex.Execute<bool>(expression).GetAwaiter().GetResult();
|
||||
Assert.Fail("Expected ExpressionExecutor to fail evaluating an expression when UserContext does not contain a necessary definitions.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Fail_BadExpression()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo";
|
||||
|
||||
Assert.ThrowsException<InvalidCastException>(() =>
|
||||
{
|
||||
ex.Execute<bool>(expression).GetAwaiter().GetResult();
|
||||
}, "Expected ExpressionExecutor to fail evaluating an expression that cannot be evaluated.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_CompileWithExternalDependencies()
|
||||
{
|
||||
this.UserContext.Foo = ExternalTestType.TestEnum;
|
||||
List<Type> dependencies = new List<Type>();
|
||||
dependencies.Add(typeof(ExternalTestType));
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, dependencies);
|
||||
string expression = "UserContext.Foo == ExternalTestType.TestEnum";
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Fail_CompileExpressionWithMissingDependencies()
|
||||
{
|
||||
this.UserContext.Foo = ExternalTestType.ExampleEnum;
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo == ExternalTestType.ExampleEnum";
|
||||
|
||||
try
|
||||
{
|
||||
ex.Execute<bool>(expression).GetAwaiter().GetResult();
|
||||
Assert.Fail("Expected ExpressionExecutor to fail evaluating an expression because runtime assembly is missing required dependencies.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Fail_CompileExpressionWithExternalDependenciesAndMissingDependencies()
|
||||
{
|
||||
this.UserContext.Foo = ExternalTestType.TestEnum;
|
||||
this.UserContext.Bar = DiffNamespaceType.TestOne;
|
||||
List<Type> dependencies = new List<Type>();
|
||||
dependencies.Add(typeof(ExternalTestType));
|
||||
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, dependencies);
|
||||
string expression = "UserContext.Foo == ExternalTestType.TestEnum && UserContext.Bar == DiffNamespaceType.TestOne";
|
||||
|
||||
try
|
||||
{
|
||||
ex.Execute<bool>(expression).GetAwaiter().GetResult();
|
||||
Assert.Fail("Expected ExpressionExecutor to fail evaluating an expression because runtime assembly is missing required dependencies.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_CompileExpressionWithMissingDependenciesButOtherExternalTypesInSameNamespace()
|
||||
{
|
||||
this.UserContext.Foo = ExternalTestType.TestEnum;
|
||||
this.UserContext.Bar = TestType.Test;
|
||||
List<Type> dependencies = new List<Type>();
|
||||
dependencies.Add(typeof(ExternalTestType));
|
||||
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, dependencies);
|
||||
string expression = "UserContext.Foo == ExternalTestType.TestEnum && UserContext.Bar == TestType.Test";
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_CompileExpressionWithForgeDefaultDependenciesBeingPassedInExternally()
|
||||
{
|
||||
this.UserContext.Foo = "Foo";
|
||||
List<Type> dependencies = new List<Type>();
|
||||
// Tasks dependency needed by Forge by default.
|
||||
dependencies.Add(typeof(Task));
|
||||
// Reflection dependency needed by Forge for runtime compilation.
|
||||
dependencies.Add(typeof(Type));
|
||||
|
||||
// Default dependencies are expected to be tossed away internally in ExpressionExecutor.
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
string expression = "UserContext.Foo == \"Foo\"";
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_ChangingFunctionDefinition()
|
||||
{
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
|
||||
// Rewritting GetCount to return 2
|
||||
string expression="UserContext.GetCount = new Func<int>(() => 2)";
|
||||
Assert.AreEqual(this.UserContext.GetCount(), 1);
|
||||
ex.Execute<Func<int>>(expression).GetAwaiter().GetResult();
|
||||
Assert.AreEqual(this.UserContext.GetCount(), 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Fail_ChangingFunctionReturnType()
|
||||
{
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
|
||||
// Changing return type of GetCount
|
||||
string expression = "UserContext.GetCount = new Func<string>(() => \"Test\")";
|
||||
Assert.ThrowsException<InvalidCastException>(() =>
|
||||
{
|
||||
// Since expected return type matches the original Func<int> type, this should throw an error
|
||||
ex.Execute<Func<int>>(expression).GetAwaiter().GetResult();
|
||||
}, "Expected ExpressionExecutor to fail evaluating can not change return type");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_ChangingFunctionReturnType()
|
||||
{
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
|
||||
// Changing return type of GetCount
|
||||
string expression = "UserContext.GetCount = new Func<string>(() => \"Test\")";
|
||||
Assert.AreEqual(this.UserContext.GetCount(), 1);
|
||||
// Since expected return type has been updated, this should pass
|
||||
ex.Execute<Func<string>>(expression).GetAwaiter().GetResult();
|
||||
Assert.AreEqual(this.UserContext.GetCount(), "Test");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Fail_ExecutingMultipleStatements()
|
||||
{
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
|
||||
// Changing return type of GetCount
|
||||
string expression = "int x = UserContext.GetCount() + 5; UserContext.GetCount = new Func<int>(() => x); return UserContext.GetCount()";
|
||||
|
||||
try
|
||||
{
|
||||
ex.Execute<Func<int>>(expression).GetAwaiter().GetResult();
|
||||
Assert.Fail("Expected ExpressionExecutor to fail evaluating can not pass multiple statements to expression.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_WaitForDelegate()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
string expression = "(Func<bool>)(() => {return UserContext.Foo == \"Bar\";})";
|
||||
|
||||
// Casting the expression to Func<bool> since the executor will return a delegate of type Func<bool>
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
dynamic expressionResult = ex.Execute<Delegate>(expression).GetAwaiter().GetResult();
|
||||
|
||||
if (expressionResult.GetType() == typeof(Func<bool>))
|
||||
{
|
||||
Assert.IsTrue(expressionResult());
|
||||
this.UserContext.Foo = "Far";
|
||||
Assert.IsFalse(expressionResult());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail(string.Format("Expected expression to be of type bool but was {0}", expressionResult.GetType()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_Success_WaitForDelegateAsync()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
string expression = "(Func<Task<bool>>)(() => {return Task.FromResult(UserContext.Foo == \"Bar\");})";
|
||||
|
||||
// Casting the expression to Func<bool> since the executor will return a delegate of type Func<bool>
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
dynamic expressionResult = ex.Execute<Delegate>(expression).GetAwaiter().GetResult();
|
||||
|
||||
if (expressionResult.GetType() == typeof(Func<Task<bool>>))
|
||||
{
|
||||
// expressionResult() return Task<bool>, doing .GetAwaiter().GetResult() again returns bool
|
||||
Assert.IsTrue(expressionResult().GetAwaiter().GetResult());
|
||||
this.UserContext.Foo = "Far";
|
||||
Assert.IsFalse(expressionResult().GetAwaiter().GetResult());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail(string.Format("Expected expression to be of type Task<bool> but was {0}", expressionResult.GetType()));
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExecutor_ScriptCacheContainsKey()
|
||||
{
|
||||
this.UserContext.Foo = "Bar";
|
||||
string expression = "UserContext.Foo == \"Bar\" && UserContext.GetTopic(\"TopicName\").ResourceType == \"Node\"";
|
||||
|
||||
// Test - confirm ExpressionExecutor script cache does not contain script before executing.
|
||||
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
|
||||
Assert.IsFalse(ex.ScriptCacheContainsKey(expression));
|
||||
|
||||
// Test - confirm ExpressionExecutor script cache does contain script after executing.
|
||||
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult());
|
||||
Assert.IsTrue(ex.ScriptCacheContainsKey(expression));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ExternalTestType.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// Test type used to test external Forge dependency injection.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.ExternalTypes
|
||||
{
|
||||
|
||||
public enum ExternalTestType
|
||||
{
|
||||
ExampleEnum = 0,
|
||||
|
||||
TestEnum = 1,
|
||||
}
|
||||
|
||||
public enum TestType
|
||||
{
|
||||
Example = 0,
|
||||
|
||||
Test = 1
|
||||
}
|
||||
}
|
||||
|
||||
namespace Forge.TreeWalker.UnitTests
|
||||
{
|
||||
public enum DiffNamespaceType
|
||||
{
|
||||
TestOne,
|
||||
|
||||
TestTwo
|
||||
}
|
||||
}
|
|
@ -0,0 +1,425 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ForgeSchemaHelper.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// Helper class for TreeWalkerUnitTests that holds ForgeSchema examples.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker.UnitTests
|
||||
{
|
||||
public static class ForgeSchemaHelper
|
||||
{
|
||||
public const string TardigradeScenario = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Selection"",
|
||||
""ChildSelector"":
|
||||
[
|
||||
{
|
||||
""ShouldSelect"": ""C#|UserContext.ResourceType == \""Node\"""",
|
||||
""Child"": ""Node""
|
||||
},
|
||||
{
|
||||
""ShouldSelect"": ""C#|UserContext.ResourceType == \""Container\"""",
|
||||
""Child"": ""Container""
|
||||
}
|
||||
]
|
||||
},
|
||||
""Container"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Container_CollectDiagnosticsAction"": {
|
||||
""Action"": ""CollectDiagnosticsAction"",
|
||||
""Input"":
|
||||
{
|
||||
""Command"": ""RunCollectDiagnostics.exe""
|
||||
}
|
||||
}
|
||||
},
|
||||
""ChildSelector"":
|
||||
[
|
||||
{
|
||||
""ShouldSelect"": ""C#|Session.GetLastActionResponse().Status == \""Success\"""",
|
||||
""Child"": ""Tardigrade""
|
||||
}
|
||||
]
|
||||
},
|
||||
""Tardigrade"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Tardigrade_TardigradeAction"": {
|
||||
""Action"": ""TardigradeAction""
|
||||
}
|
||||
},
|
||||
""ChildSelector"":
|
||||
[
|
||||
{
|
||||
""ShouldSelect"": ""C#|Session.GetLastActionResponse().Status == \""Success\"""",
|
||||
""Child"": ""Tardigrade_Success""
|
||||
}
|
||||
]
|
||||
},
|
||||
""Tardigrade_Success"": {
|
||||
""Type"": ""Leaf""
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string ActionException_Fail = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestDelayExceptionAction"": {
|
||||
""Action"": ""TestDelayExceptionAction"",
|
||||
""Input"":
|
||||
{
|
||||
""ThrowException"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string ActionException_ContinuationOnRetryExhaustion = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestDelayExceptionAction"": {
|
||||
""Action"": ""TestDelayExceptionAction"",
|
||||
""Input"":
|
||||
{
|
||||
""ThrowException"": true
|
||||
},
|
||||
""ContinuationOnRetryExhaustion"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string ActionDelay_Fail = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestDelayExceptionAction"": {
|
||||
""Action"": ""TestDelayExceptionAction"",
|
||||
""Input"":
|
||||
{
|
||||
""DelayMilliseconds"": 50
|
||||
},
|
||||
""Timeout"": 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string ActionDelay_ContinuationOnTimeout = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestDelayExceptionAction"": {
|
||||
""Action"": ""TestDelayExceptionAction"",
|
||||
""Input"":
|
||||
{
|
||||
""DelayMilliseconds"": 50
|
||||
},
|
||||
""Timeout"": 10,
|
||||
""ContinuationOnTimeout"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestDelayExceptionAction"": {
|
||||
""Action"": ""TestDelayExceptionAction"",
|
||||
""Input"":
|
||||
{
|
||||
""DelayMilliseconds"": 50,
|
||||
""ThrowException"": true
|
||||
},
|
||||
""Timeout"": 100,
|
||||
""RetryPolicy"":
|
||||
{
|
||||
""Type"": ""FixedInterval"",
|
||||
""MinBackoffMs"": 25
|
||||
},
|
||||
""ContinuationOnTimeout"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutBetweenRetries = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestDelayExceptionAction"": {
|
||||
""Action"": ""TestDelayExceptionAction"",
|
||||
""Input"":
|
||||
{
|
||||
""DelayMilliseconds"": 25,
|
||||
""ThrowException"": true
|
||||
},
|
||||
""Timeout"": 50,
|
||||
""RetryPolicy"":
|
||||
{
|
||||
""Type"": ""FixedInterval"",
|
||||
""MinBackoffMs"": 100
|
||||
},
|
||||
""ContinuationOnTimeout"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public const string NoChildMatch = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Selection"",
|
||||
""ChildSelector"":
|
||||
[
|
||||
{
|
||||
""ShouldSelect"": ""C#|false"",
|
||||
""Child"": ""LeafNode""
|
||||
}
|
||||
]
|
||||
},
|
||||
""LeafNode"": {
|
||||
""Type"": ""Leaf""
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string TestEvaluateInputTypeAction = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestEvaluateInputTypeAction"": {
|
||||
""Action"": ""TestEvaluateInputTypeAction"",
|
||||
""Input"":
|
||||
{
|
||||
""Command"": ""tasklist"",
|
||||
""IntExpression"": ""C#<Int64>|UserContext.GetCount()"",
|
||||
""BoolExpression"": ""C#|true"",
|
||||
""NestedObject"":
|
||||
{
|
||||
""Name"": ""C#|string.Format(\""{0}_{1}\"", \""MyName\"", UserContext.Name)"",
|
||||
""Value"": ""MyValue""
|
||||
},
|
||||
""ObjectArray"":
|
||||
[
|
||||
{
|
||||
""Name"": ""C#|UserContext.Name"",
|
||||
""Value"": ""FirstValue""
|
||||
},
|
||||
{
|
||||
""Name"": ""SecondName"",
|
||||
""Value"": ""SecondValue""
|
||||
}
|
||||
],
|
||||
""StringArray"": [""value1"", ""value2""],
|
||||
""LongArray"": [1, 3, 2],
|
||||
""BoolDelegate"": ""C#|(Func<bool>)(() => {return UserContext.GetCount() == 1;})"",
|
||||
""BoolDelegateAsync"": ""C#|(Func<Task<bool>>)(async() => { return await UserContext.GetCountAsync() == 2; })""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string TestEvaluateInputType_FailOnField_Action = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestEvaluateInputType_FailOnField_Action"": {
|
||||
""Action"": ""TestEvaluateInputType_FailOnField_Action"",
|
||||
""Input"":
|
||||
{
|
||||
""UnexpectedField"": true
|
||||
},
|
||||
""ContinuationOnRetryExhaustion"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string TestEvaluateInputTypeAction_UnexpectedPropertyFail = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestEvaluateInputTypeAction"": {
|
||||
""Action"": ""TestEvaluateInputTypeAction"",
|
||||
""Input"":
|
||||
{
|
||||
""UnexpectedProperty"": true
|
||||
},
|
||||
""ContinuationOnRetryExhaustion"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string TestEvaluateInputType_FailOnNonEmptyCtor_Action = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_TestEvaluateInputType_FailOnNonEmptyCtor_Action"": {
|
||||
""Action"": ""TestEvaluateInputType_FailOnNonEmptyCtor_Action"",
|
||||
""Input"":
|
||||
{
|
||||
""BoolProperty"": true
|
||||
},
|
||||
""ContinuationOnRetryExhaustion"": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string LeafNodeSummaryAction = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Leaf"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_LeafNodeSummaryAction"": {
|
||||
""Action"": ""LeafNodeSummaryAction"",
|
||||
""Input"":
|
||||
{
|
||||
""Status"": ""Success"",
|
||||
""StatusCode"": 1,
|
||||
""Output"": ""TheResult""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string LeafNodeSummaryAction_InputIsActionResponse = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Action"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_CollectDiagnosticsAction"": {
|
||||
""Action"": ""CollectDiagnosticsAction"",
|
||||
""Input"":
|
||||
{
|
||||
""Command"": ""TheCommand""
|
||||
}
|
||||
}
|
||||
},
|
||||
""ChildSelector"":
|
||||
[
|
||||
{
|
||||
""Child"": ""LeafNodeSummaryTest""
|
||||
}
|
||||
]
|
||||
},
|
||||
""LeafNodeSummaryTest"": {
|
||||
""Type"": ""Leaf"",
|
||||
""Actions"":
|
||||
{
|
||||
""LeafNodeSummaryTest_LeafNodeSummaryAction"": {
|
||||
""Action"": ""LeafNodeSummaryAction"",
|
||||
""Input"": ""C#|Session.GetLastActionResponse()""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
public const string ExternalExecutors = @"
|
||||
{
|
||||
""Tree"":
|
||||
{
|
||||
""Root"": {
|
||||
""Type"": ""Leaf"",
|
||||
""Actions"":
|
||||
{
|
||||
""Root_LeafNodeSummaryAction"": {
|
||||
""Action"": ""LeafNodeSummaryAction"",
|
||||
""Input"":
|
||||
{
|
||||
""Status"": ""External|StatusResult""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,762 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TreeWalkerUnitTests.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// Tests the TreeWalkerSession class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker.UnitTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Forge.Attributes;
|
||||
using Forge.DataContracts;
|
||||
using Forge.TreeWalker;
|
||||
using Forge.TreeWalker.ForgeExceptions;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
[TestClass]
|
||||
public class TreeWalkerUnitTests
|
||||
{
|
||||
private Guid sessionId;
|
||||
private IForgeDictionary forgeState = new ForgeDictionary(new Dictionary<string, object>(), Guid.Empty);
|
||||
private dynamic UserContext = new System.Dynamic.ExpandoObject();
|
||||
private ITreeWalkerCallbacks callbacks;
|
||||
private CancellationToken token;
|
||||
private TreeWalkerParameters parameters;
|
||||
private TreeWalkerSession session;
|
||||
|
||||
public void TestInitialize(string jsonSchema)
|
||||
{
|
||||
// Initialize contexts, callbacks, and actions.
|
||||
this.sessionId = Guid.NewGuid();
|
||||
this.forgeState = new ForgeDictionary(new Dictionary<string, object>(), this.sessionId);
|
||||
this.callbacks = new TreeWalkerCallbacks();
|
||||
this.token = new CancellationTokenSource().Token;
|
||||
|
||||
this.UserContext.Name = "MyName";
|
||||
this.UserContext.ResourceType = "Container";
|
||||
|
||||
this.UserContext.GetCount = (Func<Int32>)(() =>
|
||||
{
|
||||
return 1;
|
||||
});
|
||||
|
||||
this.UserContext.GetCountAsync = (Func<Task<Int32>>)(() =>
|
||||
{
|
||||
return Task.FromResult(2);
|
||||
});
|
||||
|
||||
this.parameters = new TreeWalkerParameters(
|
||||
this.sessionId,
|
||||
jsonSchema,
|
||||
this.forgeState,
|
||||
this.callbacks,
|
||||
this.token)
|
||||
{
|
||||
UserContext = this.UserContext,
|
||||
ForgeActionsAssembly = typeof(CollectDiagnosticsAction).Assembly
|
||||
};
|
||||
|
||||
this.session = new TreeWalkerSession(this.parameters);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_Constructor()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test 1 - Verify jsonSchema was successfully deserialized in constructor.
|
||||
Assert.AreEqual("Action", this.session.Schema.Tree["Tardigrade"].Type.ToString());
|
||||
|
||||
// Test 2 - Verify the Status is Initialized.
|
||||
Assert.AreEqual("Initialized", this.session.Status, "Expected WalkTree status to be Initialized after initializing TreeWalkerSession.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_VisitNode_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test - VisitNode and expect the first child to be returned.
|
||||
string expected = "Tardigrade";
|
||||
string actualNextTreeNodeKey = this.session.VisitNode("Container").GetAwaiter().GetResult();
|
||||
|
||||
Assert.AreEqual(expected, actualNextTreeNodeKey, "Expected VisitNode(Container) to return Tardigrade.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_VisitNode_LeafNode_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test - VisitNode on node of Leaf type and confirm it does not throw.
|
||||
string expected = null;
|
||||
string actualNextTreeNodeKey = this.session.VisitNode("Tardigrade_Success").GetAwaiter().GetResult();
|
||||
|
||||
Assert.AreEqual(expected, actualNextTreeNodeKey, "Expected VisitNode(Tardigrade_Success) to return without throwing.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_VisitNode_NoTimeout_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test - VisitNode with no Timeout and execute an Action with no Timeout set. Confirm we do not throw exceptions.
|
||||
string expected = "Tardigrade_Success";
|
||||
string actualNextTreeNodeKey = this.session.VisitNode("Tardigrade").GetAwaiter().GetResult();
|
||||
Assert.AreEqual(expected, actualNextTreeNodeKey, "Expected VisitNode(Tardigrade) to return Tardigrade_Success without throwing exception.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test - WalkTree and expect the Status to be RanToCompletion.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to run to completion.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionThrowsException_TimeoutOnAction()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action that throws exception.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionException_Fail);
|
||||
|
||||
// Test - WalkTree and expect the Status to be TimeoutOnAction due to unexpected exceptions thrown in action.
|
||||
Assert.ThrowsException<ActionTimeoutException>(() =>
|
||||
{
|
||||
string temp = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to timeout on action because the Action threw exceptions with no Continuation flags set.");
|
||||
|
||||
string actualStatus = this.session.Status;
|
||||
Assert.AreEqual("TimeoutOnAction", actualStatus, "Expected WalkTree to timeout on action because the Action threw exceptions with no Continuation flags set.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionThrowsException_ContinuationOnRetryExhaustion()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action that throws exception but has ContinuationOnRetryExhaustion flag set.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionException_ContinuationOnRetryExhaustion);
|
||||
|
||||
// Test - Expect WalkTree to be successful because the TreeAction exhausted retries but ContinuationOnRetryExhaustion flag was set.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction exhausted retries but ContinuationOnRetryExhaustion flag was set.");
|
||||
|
||||
ActionResponse actionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual("RetryExhaustedOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction exhausted retries but ContinuationOnRetryExhaustion flag was set.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_TimeoutOnAction()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action that will time out.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_Fail);
|
||||
|
||||
// Test - WalkTree and expect the Status to be TimeoutOnAction due to Action timing out.
|
||||
Assert.ThrowsException<ActionTimeoutException>(() =>
|
||||
{
|
||||
string temp = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to timeout on action because the Action timed out with no Continuation flags set.");
|
||||
|
||||
string actualStatus = this.session.Status;
|
||||
Assert.AreEqual("TimeoutOnAction", actualStatus, "Expected WalkTree to timeout on action because the Action timed out with no Continuation flags set.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_ContinuationOnTimeout()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action that will time out but has ContinuationOnTimeout flag set.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout);
|
||||
|
||||
// Test - Expect WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
|
||||
|
||||
ActionResponse actionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual("TimeoutOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action with RetryPolicy that will time out inside the Action but has ContinuationOnTimeout flag set.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction);
|
||||
|
||||
// Test - Expect WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
|
||||
|
||||
ActionResponse actionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual("TimeoutOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_ContinuationOnTimeout_RetryPolicy_TimeoutBetweenRetries()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action with RetryPolicy that will time out between retry attempts but has ContinuationOnTimeout flag set.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutBetweenRetries);
|
||||
|
||||
// Test - Expect WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
|
||||
|
||||
ActionResponse actionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual("TimeoutOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_CancelledBeforeExecution()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test - CancelWalkTree before WalkTree and expect the Status to be CancelledBeforeExecution.
|
||||
this.session.CancelWalkTree();
|
||||
|
||||
Assert.ThrowsException<TaskCanceledException>(() =>
|
||||
{
|
||||
string temp = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to throw exception after calling CancelWalkTree.");
|
||||
|
||||
string actualStatus = this.session.Status;
|
||||
Assert.AreEqual("CancelledBeforeExecution", actualStatus, "Expected WalkTree to be cancelled before execution after calling CancelWalkTree.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_ActionWithDelay_CancelWalkTree()
|
||||
{
|
||||
// Initialize TreeWalkerSession with a schema containing Action with delay.
|
||||
// This gives us time to start WalkTree before calling CancelWalkTree.
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction);
|
||||
|
||||
// Test - WalkTree then CancelWalkTree while WalkTree is running and expect the Status to be Cancelled.
|
||||
Task<string> task = this.session.WalkTree("Root");
|
||||
Thread.Sleep(25);
|
||||
this.session.CancelWalkTree();
|
||||
Assert.ThrowsException<OperationCanceledException>(() =>
|
||||
{
|
||||
string temp = task.GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to throw exception after calling CancelWalkTree.");
|
||||
|
||||
string actualStatus = this.session.Status;
|
||||
Assert.AreEqual("Cancelled", actualStatus, "Expected WalkTree to be cancelled after calling CancelWalkTree.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_Failed_MissingKey()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test - WalkTree and expect the Status to be Failed because the key does not exist which threw an exception.
|
||||
Assert.ThrowsException<KeyNotFoundException>(() =>
|
||||
{
|
||||
string temp = this.session.WalkTree("MissingKey").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to fail because the key does not exist.");
|
||||
|
||||
string actualStatus = this.session.Status;
|
||||
Assert.AreEqual("Failed", actualStatus, "Expected WalkTree to fail because the key does not exist.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTreeWalkerSession_WalkTree_NoChildMatched()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.NoChildMatch);
|
||||
|
||||
// Test - WalkTree and expect the Status to be NoChildMatched.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion_NoChildMatched", actualStatus, "Expected WalkTree to end with NoChildMatched status.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetCurrentTreeNode()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test 1 - Confirm GetCurrentTreeNode returns null before walking tree.
|
||||
Assert.AreEqual(null, this.session.GetCurrentTreeNode().GetAwaiter().GetResult(), "Expected CurrentTreeNode to be null before starting walk tree.");
|
||||
|
||||
// Test 2 - Confirm GetCurrentTreeNode returns last node visited after walking tree.
|
||||
this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("Tardigrade_Success", this.session.GetCurrentTreeNode().GetAwaiter().GetResult(), "Expected CurrentTreeNode to equal the last node visited.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetLastTreeAction()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Test 1 - Confirm GetLastTreeAction returns null before walking tree.
|
||||
Assert.AreEqual(null, this.session.GetLastTreeAction().GetAwaiter().GetResult(), "Expected LastTreeAction to be null before starting walk tree.");
|
||||
|
||||
// Test 2 - Confirm GetLastTreeAction returns last tree action executed after walking tree.
|
||||
this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("Tardigrade_TardigradeAction", this.session.GetLastTreeAction().GetAwaiter().GetResult(), "Expected LastTreeAction to equal the last tree action executed.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetOutput()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to run to completion.");
|
||||
|
||||
// Test 1 - Confirm ActionResponse can be read from GetOutputAsync.
|
||||
ActionResponse actionResponse = this.session.GetOutputAsync("Container_CollectDiagnosticsAction").GetAwaiter().GetResult();
|
||||
Assert.AreEqual(
|
||||
"RunCollectDiagnostics.exe_Results",
|
||||
actionResponse.Output,
|
||||
"Expected to successfully read ActionResponse.Output.");
|
||||
|
||||
Assert.AreEqual(
|
||||
"Success",
|
||||
actionResponse.Status,
|
||||
"Expected to successfully read ActionResponse.Status.");
|
||||
|
||||
// Test 2 - Confirm ActionResponse can be read from GetOutput.
|
||||
actionResponse = this.session.GetOutput("Container_CollectDiagnosticsAction");
|
||||
Assert.AreEqual(
|
||||
"RunCollectDiagnostics.exe_Results",
|
||||
actionResponse.Output,
|
||||
"Expected to successfully read ActionResponse.Output.");
|
||||
|
||||
Assert.AreEqual(
|
||||
"Success",
|
||||
actionResponse.Status,
|
||||
"Expected to successfully read ActionResponse.Status.");
|
||||
|
||||
// Test 3 - Confirm ActionResponse can be read from GetLastActionResponseAsync.
|
||||
actionResponse = this.session.GetLastActionResponseAsync().GetAwaiter().GetResult();
|
||||
Assert.AreEqual(
|
||||
"Success",
|
||||
actionResponse.Status,
|
||||
"Expected to successfully read ActionResponse.Status.");
|
||||
|
||||
// Test 4 - Confirm ActionResponse can be read from GetLastActionResponse.
|
||||
actionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual(
|
||||
"Success",
|
||||
actionResponse.Status,
|
||||
"Expected to successfully read ActionResponse.Status.");
|
||||
}
|
||||
|
||||
// TODO: Add back test once we decide how we want to handle this case: Should people be able to change function definitions?
|
||||
// TestScript in local jsonSchema is related to this test.
|
||||
// [TestMethod]
|
||||
// public void TestTreeWalkerSession_WalkTree_Success_ChangingFunctionDefinition()
|
||||
// {
|
||||
// this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
|
||||
|
||||
// Assert.AreEqual(this.UserContext.GetCount(), 1);
|
||||
// string actual = this.session.WalkTree("TestScript").GetAwaiter().GetResult();
|
||||
// Assert.AreEqual(2, this.UserContext.GetCount());
|
||||
// }
|
||||
|
||||
// TODO: Add back test once we decide how we want to handle this case: Should people be able to change function definitions?
|
||||
// TestScript in local jsonSchema is related to this test.
|
||||
|
||||
[TestMethod]
|
||||
public void Test_EvaluateInputType_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputTypeAction);
|
||||
|
||||
// Test - WalkTree to execute an Action with its ActionInput type defined in the ActionDefinition.InputType.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus);
|
||||
|
||||
Assert.AreEqual(
|
||||
true,
|
||||
this.session.GetLastActionResponse().Output,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_EvaluateInputType_UnexpectedFieldFail()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputType_FailOnField_Action);
|
||||
|
||||
// Test - WalkTree and expect the Status to be Failed_EvaluateDynamicProperty
|
||||
// because ActionInput type defined in the ActionDefinition.InputType contained an unexpected public Field.
|
||||
string actual;
|
||||
Assert.ThrowsException<EvaluateDynamicPropertyException>(() =>
|
||||
{
|
||||
actual = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to fail because ActionInput type defined in the ActionDefinition.InputType contained an unexpected public Field.");
|
||||
|
||||
actual = this.session.Status;
|
||||
Assert.AreEqual(
|
||||
"Failed_EvaluateDynamicProperty",
|
||||
actual,
|
||||
"Expected WalkTree to fail because ActionInput type defined in the ActionDefinition.InputType contained an unexpected public Field.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_EvaluateInputType_UnexpectedPropertyFail()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputTypeAction_UnexpectedPropertyFail);
|
||||
|
||||
// Test - WalkTree and expect the Status to be Failed_EvaluateDynamicProperty
|
||||
// because the schema contained a Property that does not exist in ActionDefinition.InputType.
|
||||
string actual;
|
||||
Assert.ThrowsException<EvaluateDynamicPropertyException>(() =>
|
||||
{
|
||||
actual = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to fail because the schema contained a Property that does not exist in ActionDefinition.InputType.");
|
||||
|
||||
actual = this.session.Status;
|
||||
Assert.AreEqual(
|
||||
"Failed_EvaluateDynamicProperty",
|
||||
actual,
|
||||
"Expected WalkTree to fail because the schema contained a Property that does not exist in ActionDefinition.InputType.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_EvaluateInputType_ParameterizedConstructorFail()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputType_FailOnNonEmptyCtor_Action);
|
||||
|
||||
// Test - WalkTree and expect the Status to be Failed_EvaluateDynamicProperty
|
||||
// because its ActionDefinition.InputType did not have a parameterless constructor.
|
||||
string actual;
|
||||
Assert.ThrowsException<EvaluateDynamicPropertyException>(() =>
|
||||
{
|
||||
actual = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
}, "Expected WalkTree to fail because its ActionDefinition.InputType did not have a parameterless constructor.");
|
||||
|
||||
actual = this.session.Status;
|
||||
Assert.AreEqual(
|
||||
"Failed_EvaluateDynamicProperty",
|
||||
actual,
|
||||
"Expected WalkTree to fail because its ActionDefinition.InputType did not have a parameterless constructor.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_LeafNodeSummaryAction_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.LeafNodeSummaryAction);
|
||||
|
||||
// Test - WalkTree to execute a LeafNodeSummaryAction node with its ActionInput set to ActionResponse properties.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus);
|
||||
|
||||
ActionResponse leafActionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual(
|
||||
"Success",
|
||||
leafActionResponse.Status,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
|
||||
Assert.AreEqual(
|
||||
1,
|
||||
leafActionResponse.StatusCode,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
|
||||
Assert.AreEqual(
|
||||
"TheResult",
|
||||
leafActionResponse.Output,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_LeafNodeSummaryAction_InputAsObject_Success()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.LeafNodeSummaryAction_InputIsActionResponse);
|
||||
|
||||
// Test - WalkTree to execute a LeafNodeSummaryAction node with its ActionInput set to ActionResponse object of the previously ran Action in the parent node.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus);
|
||||
|
||||
ActionResponse leafActionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual(
|
||||
"Success",
|
||||
leafActionResponse.Status,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
|
||||
Assert.AreEqual(
|
||||
"TheCommand_Results",
|
||||
leafActionResponse.Output,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ExternalExecutors()
|
||||
{
|
||||
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ExternalExecutors);
|
||||
|
||||
Dictionary<string, Func<string, CancellationToken, Task<object>>> externalExecutors = new Dictionary<string, Func<string, CancellationToken, Task<object>>>();
|
||||
externalExecutors.Add("External|", External);
|
||||
|
||||
this.parameters.ExternalExecutors = externalExecutors;
|
||||
this.session = new TreeWalkerSession(this.parameters);
|
||||
|
||||
// Test - WalkTree to execute an Action with an ActionInput that uses an external executor. Confirm expected results.
|
||||
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
|
||||
Assert.AreEqual("RanToCompletion", actualStatus);
|
||||
|
||||
ActionResponse leafActionResponse = this.session.GetLastActionResponse();
|
||||
Assert.AreEqual(
|
||||
"StatusResult_Executed",
|
||||
leafActionResponse.Status,
|
||||
"Expected to successfully retrieve the Func output value from the action.");
|
||||
}
|
||||
|
||||
private static async Task<object> External(string expression, CancellationToken token)
|
||||
{
|
||||
// External executes the expression and returns.
|
||||
await Task.Delay(1, token);
|
||||
return expression + "_Executed";
|
||||
}
|
||||
|
||||
private sealed class TreeWalkerCallbacks : ITreeWalkerCallbacks
|
||||
{
|
||||
public async Task BeforeVisitNode(
|
||||
Guid sessionId,
|
||||
string treeNodeKey,
|
||||
dynamic properties,
|
||||
dynamic userContext,
|
||||
CancellationToken token)
|
||||
{
|
||||
string serializeProperties = JsonConvert.SerializeObject(properties);
|
||||
|
||||
await Task.Run(() => Console.WriteLine(string.Format(
|
||||
"OnBeforeVisitNode: SessionId: {0}, TreeNodeKey: {1}, Properties: {2}.",
|
||||
sessionId,
|
||||
treeNodeKey,
|
||||
serializeProperties)));
|
||||
}
|
||||
|
||||
public Task AfterVisitNode(
|
||||
Guid sessionId,
|
||||
string treeNodeKey,
|
||||
dynamic properties,
|
||||
dynamic userContext,
|
||||
CancellationToken token)
|
||||
{
|
||||
Console.WriteLine(string.Format(
|
||||
"OnAfterVisitNode: SessionId: {0}, TreeNodeKey: {1}, Properties: {2}.",
|
||||
sessionId,
|
||||
treeNodeKey,
|
||||
JsonConvert.SerializeObject(properties)));
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseCommonAction : BaseAction
|
||||
{
|
||||
public object Input { get; private set; }
|
||||
|
||||
public CancellationToken Token { get; private set; }
|
||||
|
||||
public Guid SessionId { get; private set; }
|
||||
|
||||
public string TreeNodeKey { get; private set; }
|
||||
|
||||
private ActionContext actionContext;
|
||||
|
||||
public override Task<ActionResponse> RunAction(ActionContext actionContext)
|
||||
{
|
||||
this.Input = actionContext.ActionInput;
|
||||
this.Token = actionContext.Token;
|
||||
this.SessionId = actionContext.SessionId;
|
||||
this.TreeNodeKey = actionContext.TreeNodeKey;
|
||||
this.actionContext = actionContext;
|
||||
|
||||
return this.RunAction();
|
||||
}
|
||||
|
||||
public abstract Task<ActionResponse> RunAction();
|
||||
|
||||
public Task CommitIntermediates<T>(T intermediates)
|
||||
{
|
||||
return this.actionContext.CommitIntermediates<T>(intermediates);
|
||||
}
|
||||
|
||||
public Task<T> GetIntermediates<T>()
|
||||
{
|
||||
return this.actionContext.GetIntermediates<T>();
|
||||
}
|
||||
}
|
||||
|
||||
[ForgeAction(InputType: typeof(CollectDiagnosticsInput))]
|
||||
public class CollectDiagnosticsAction : BaseCommonAction
|
||||
{
|
||||
public override async Task<ActionResponse> RunAction()
|
||||
{
|
||||
CollectDiagnosticsInput actionInput = (CollectDiagnosticsInput)this.Input;
|
||||
|
||||
// Collect diagnostics using the input command and commit results.
|
||||
string intermediates = this.GetIntermediates<string>().GetAwaiter().GetResult();
|
||||
Assert.AreEqual(null, intermediates);
|
||||
|
||||
string result = MockCollectDiagnosticsResult(actionInput.Command);
|
||||
|
||||
intermediates = result;
|
||||
this.CommitIntermediates<string>(intermediates).GetAwaiter().GetResult();
|
||||
Assert.AreEqual(intermediates, this.GetIntermediates<string>().GetAwaiter().GetResult());
|
||||
|
||||
await Task.Run(() => Console.WriteLine(string.Format(
|
||||
"CollectDiagnosticsAction - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
|
||||
this.SessionId,
|
||||
this.TreeNodeKey,
|
||||
JsonConvert.SerializeObject(actionInput))));
|
||||
|
||||
ActionResponse actionResponse = new ActionResponse() { Status = "Success", Output = result };
|
||||
return actionResponse;
|
||||
}
|
||||
|
||||
private static string MockCollectDiagnosticsResult(string command)
|
||||
{
|
||||
return command + "_Results";
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectDiagnosticsInput
|
||||
{
|
||||
public string Command { get; set; }
|
||||
}
|
||||
|
||||
[ForgeAction]
|
||||
public class TardigradeAction : BaseCommonAction
|
||||
{
|
||||
public override async Task<ActionResponse> RunAction()
|
||||
{
|
||||
await Task.Run(() => Console.WriteLine(string.Format(
|
||||
"TardigradeAction - SessionId: {0}, TreeNodeKey: {1}.",
|
||||
this.SessionId,
|
||||
this.TreeNodeKey)));
|
||||
|
||||
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
|
||||
return actionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
[ForgeAction(InputType: typeof(TestDelayExceptionInput))]
|
||||
public class TestDelayExceptionAction : BaseCommonAction
|
||||
{
|
||||
public override async Task<ActionResponse> RunAction()
|
||||
{
|
||||
TestDelayExceptionInput actionInput = (TestDelayExceptionInput)this.Input;
|
||||
Console.WriteLine(string.Format(
|
||||
"TestDelayExceptionAction - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
|
||||
this.SessionId,
|
||||
this.TreeNodeKey,
|
||||
JsonConvert.SerializeObject(actionInput)));
|
||||
|
||||
await Task.Delay(actionInput.DelayMilliseconds, this.Token);
|
||||
|
||||
if (actionInput.ThrowException)
|
||||
{
|
||||
throw new NullReferenceException("Throwing unexpected Exception!!");
|
||||
}
|
||||
|
||||
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
|
||||
return actionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestDelayExceptionInput
|
||||
{
|
||||
public int DelayMilliseconds { get; set; } = 0;
|
||||
|
||||
public bool ThrowException { get; set; } = false;
|
||||
}
|
||||
|
||||
[ForgeAction(InputType: typeof(FooActionInput))]
|
||||
public class TestEvaluateInputTypeAction : BaseCommonAction
|
||||
{
|
||||
public override async Task<ActionResponse> RunAction()
|
||||
{
|
||||
FooActionInput actionInput = (FooActionInput)this.Input;
|
||||
bool boolDelegateResult = actionInput.BoolDelegate();
|
||||
bool boolDelegateAsyncResult = await actionInput.BoolDelegateAsync();
|
||||
|
||||
Console.WriteLine(string.Format(
|
||||
"TestEvaluateInputTypeAction - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}, BoolDelegateResult: {3}, BoolDelegateAsyncResult: {4}.",
|
||||
this.SessionId,
|
||||
this.TreeNodeKey,
|
||||
JsonConvert.SerializeObject(actionInput),
|
||||
boolDelegateResult,
|
||||
boolDelegateAsyncResult));
|
||||
|
||||
ActionResponse actionResponse = new ActionResponse() { Status = "Success", Output = boolDelegateResult && boolDelegateAsyncResult };
|
||||
return actionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
public class FooActionInput
|
||||
{
|
||||
public string Command { get; set; }
|
||||
public int IntExpression { get; set; }
|
||||
public bool BoolExpression { get; set; }
|
||||
public string PropertyNotInSchema { get; set; }
|
||||
public FooActionObject NestedObject { get; set; }
|
||||
public FooActionObject[] ObjectArray { get; set; }
|
||||
public string[] StringArray { get; set; }
|
||||
public long[] LongArray { get; set; }
|
||||
public Func<bool> BoolDelegate { get; set; }
|
||||
public Func<Task<bool>> BoolDelegateAsync { get; set; }
|
||||
}
|
||||
|
||||
public class FooActionObject
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
[ForgeAction(InputType: typeof(FooActionInput_UnexpectedField))]
|
||||
public class TestEvaluateInputType_FailOnField_Action : BaseCommonAction
|
||||
{
|
||||
public override async Task<ActionResponse> RunAction()
|
||||
{
|
||||
FooActionInput_UnexpectedField actionInput = (FooActionInput_UnexpectedField)this.Input;
|
||||
|
||||
await Task.Run(() => Console.WriteLine(string.Format(
|
||||
"TestEvaluateInputType_FailOnField_Action - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
|
||||
this.SessionId,
|
||||
this.TreeNodeKey,
|
||||
JsonConvert.SerializeObject(actionInput))));
|
||||
|
||||
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
|
||||
return actionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
public class FooActionInput_UnexpectedField
|
||||
{
|
||||
public bool UnexpectedField = false;
|
||||
}
|
||||
|
||||
[ForgeAction(InputType: typeof(FooActionInput_NonEmptyConstructor))]
|
||||
public class TestEvaluateInputType_FailOnNonEmptyCtor_Action : BaseCommonAction
|
||||
{
|
||||
public override async Task<ActionResponse> RunAction()
|
||||
{
|
||||
FooActionInput_NonEmptyConstructor actionInput = (FooActionInput_NonEmptyConstructor)this.Input;
|
||||
|
||||
await Task.Run(() => Console.WriteLine(string.Format(
|
||||
"TestEvaluateInputType_FailOnNonEmptyCtor_Action - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
|
||||
this.SessionId,
|
||||
this.TreeNodeKey,
|
||||
JsonConvert.SerializeObject(actionInput))));
|
||||
|
||||
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
|
||||
return actionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
public class FooActionInput_NonEmptyConstructor
|
||||
{
|
||||
public bool BoolProperty { get; set; }
|
||||
|
||||
public FooActionInput_NonEmptyConstructor(int unexpectedParameter) {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Forge.TreeWalker</RootNamespace>
|
||||
<AssemblyName>Forge.TreeWalker</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Collections.Immutable, Version=1.1.37.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO" />
|
||||
<Reference Include="System.Reflection" />
|
||||
<Reference Include="System.Reflection.Metadata, Version=1.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Threading.Tasks" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Runtime" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Microsoft.CodeAnalysis, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CodeAnalysis.CSharp, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CodeAnalysis.CSharp.Scripting, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.CodeAnalysis.CSharp.Scripting.1.3.2\lib\dotnet\Microsoft.CodeAnalysis.CSharp.Scripting.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CodeAnalysis.Scripting, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.CodeAnalysis.Scripting.Common.1.3.2\lib\dotnet\Microsoft.CodeAnalysis.Scripting.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="src\ActionContext.cs" />
|
||||
<Compile Include="src\ActionDefinition.cs" />
|
||||
<Compile Include="src\ActionResponse.cs" />
|
||||
<Compile Include="src\BaseAction.cs" />
|
||||
<Compile Include="src\ExpressionExecutor.cs" />
|
||||
<Compile Include="src\ForgeActionAttribute.cs" />
|
||||
<Compile Include="src\ForgeDictionary.cs" />
|
||||
<Compile Include="src\ForgeExceptions.cs" />
|
||||
<Compile Include="src\IForgeDictionary.cs" />
|
||||
<Compile Include="src\ITreeSession.cs" />
|
||||
<Compile Include="src\ITreeWalkerCallbacks.cs" />
|
||||
<Compile Include="src\TreeWalkerParameters.cs" />
|
||||
<Compile Include="src\TreeWalkerSession.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Forge.DataContracts\Forge.DataContracts.csproj">
|
||||
<Project>{c49a8494-13e5-4214-8434-708ba280c5b1}</Project>
|
||||
<Name>Forge.DataContracts</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -0,0 +1,38 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Forge.TreeWalker.UnitTests")]
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Forge")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Forge")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("00fc1c22-6ae9-4f60-8a3e-05885ba34c9c")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.2.3.0" newVersion="1.2.3.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.CodeAnalysis" version="1.3.2" targetFramework="net462" />
|
||||
<package id="Microsoft.CodeAnalysis.CSharp" version="1.3.2" targetFramework="net462" />
|
||||
<package id="Microsoft.CodeAnalysis.CSharp.Scripting" version="1.3.2" targetFramework="net462" />
|
||||
<package id="Microsoft.CodeAnalysis.Scripting" version="1.3.2" targetFramework="net462" />
|
||||
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net462" />
|
||||
<package id="System.Collections.Immutable" version="1.1.37" targetFramework="net462" />
|
||||
<package id="System.Reflection.Metadata" version="1.2.0" targetFramework="net462" />
|
||||
</packages>
|
|
@ -0,0 +1,134 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ActionContext.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ActionContext class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The ActionContext class object is passed to Actions to give them contextual data and methods to help them execute.
|
||||
/// </summary>
|
||||
public class ActionContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this tree walking session.
|
||||
/// </summary>
|
||||
public Guid SessionId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tree node key where the Action resides.
|
||||
/// </summary>
|
||||
public string TreeNodeKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tree action key of this Action.
|
||||
/// </summary>
|
||||
public string TreeActionKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this Action.
|
||||
/// </summary>
|
||||
public string ActionName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dynamic input for the Action given by the ForgeTree schema.
|
||||
/// </summary>
|
||||
public object ActionInput { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dynamic properties of this Action given by the ForgeTree schema.
|
||||
/// </summary>
|
||||
public object Properties { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dynamic user-defined context object that is able to be referenced when evaluating schema expressions or performing actions.
|
||||
/// </summary>
|
||||
public object UserContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token.
|
||||
/// </summary>
|
||||
public CancellationToken Token { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The forgeState dictionary that holds information relevant to Forge and Actions.
|
||||
/// </summary>
|
||||
private IForgeDictionary forgeState;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates an ActionContext object.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The unique identifier for this tree walking session.</param>
|
||||
/// <param name="treeNodeKey">The TreeNode's key where the Action is taking place.</param>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the Action taking place.</param>
|
||||
/// <param name="actionName">The name of the Action.</param>
|
||||
/// <param name="actionInput">The input for this Action.</param>
|
||||
/// <param name="properties">The properties of this Action.</param>
|
||||
/// <param name="userContext">The user context for this Action.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <param name="forgeState">The forge state dictionary.</param>
|
||||
public ActionContext(
|
||||
Guid sessionId,
|
||||
string treeNodeKey,
|
||||
string treeActionKey,
|
||||
string actionName,
|
||||
object actionInput,
|
||||
object properties,
|
||||
object userContext,
|
||||
CancellationToken token,
|
||||
IForgeDictionary forgeState)
|
||||
{
|
||||
if (sessionId == null) throw new ArgumentNullException("sessionId");
|
||||
if (string.IsNullOrWhiteSpace(treeNodeKey)) throw new ArgumentNullException("treeNodeKey");
|
||||
if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentNullException("actionName");
|
||||
if (userContext == null) throw new ArgumentNullException("userContext");
|
||||
if (token == null) throw new ArgumentNullException("token");
|
||||
if (forgeState == null) throw new ArgumentNullException("forgeState");
|
||||
|
||||
this.SessionId = sessionId;
|
||||
this.TreeNodeKey = treeNodeKey;
|
||||
this.TreeActionKey = treeActionKey;
|
||||
this.ActionName = actionName;
|
||||
this.ActionInput = actionInput;
|
||||
this.Properties = properties;
|
||||
this.UserContext = userContext;
|
||||
this.Token = token;
|
||||
this.forgeState = forgeState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits an Intermediates object for this Action to the forgeState.
|
||||
/// Since Intermediates are available to the Action on each retry, this allows Actions to persist state across retries.
|
||||
/// </summary>
|
||||
/// <param name="intermediates">The intermediates object to be committed for this Action.</param>
|
||||
public Task CommitIntermediates<T>(T intermediates)
|
||||
{
|
||||
return this.forgeState.Set<T>(this.TreeActionKey + TreeWalkerSession.IntermediatesSuffix, intermediates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the previously committed Intermediates data for this Action from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The Intermediates data for this Action if it exists, otherwise default(T).</returns>
|
||||
public async Task<T> GetIntermediates<T>()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.forgeState.GetValue<T>(this.TreeActionKey + TreeWalkerSession.IntermediatesSuffix).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ActionDefinition.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ActionDefinition class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The ActionDefinition class holds definitions for the action.
|
||||
/// </summary>
|
||||
public class ActionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// The Type of the ForgeAction class.
|
||||
/// </summary>
|
||||
public Type ActionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The InputType for this Action that will be passed to the Action by Forge when executing the Action.
|
||||
/// When given, Forge will instantiate this type when evaluating the ActionInput from the schema.
|
||||
/// When null, Forge will instead create a dynamic object.
|
||||
/// Restrictions: Only the public Properties of the InputType will be instantiated.
|
||||
/// Objects lacking a parameterless constructor are not supported.
|
||||
/// Objects with public fields are not supported.
|
||||
/// </summary>
|
||||
public Type InputType { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ActionResponse.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ActionResponse class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
/// <summary>
|
||||
/// The ActionResponse class holds the response information from actions.
|
||||
/// </summary>
|
||||
public class ActionResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The status code of this action response.
|
||||
/// </summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status of this action response.
|
||||
/// </summary>
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dynamic output of this action response.
|
||||
/// </summary>
|
||||
public object Output { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="BaseAction.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The BaseAction abstract class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The BaseAction abstract class must be inherited by all ForgeActions tagged with the ForgeActionAttribute.
|
||||
/// All ForgeActionAttribute tagged classes should reside in the Assembly that is passed to Forge TreeWalkerSession.
|
||||
/// When Forge encounters an ActionName while walking the tree, it will instantiate the ForgeAction type and then call RunAction.
|
||||
/// </summary>
|
||||
public abstract class BaseAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The RunAction method is called when Forge encounters an ActionName while walking the tree.
|
||||
/// </summary>
|
||||
/// <param name="actionContext">The action context holding relevant information for this Action.</param>
|
||||
public abstract Task<ActionResponse> RunAction(ActionContext actionContext);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ExpressionExecutor.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ExpressionExecutor class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
|
||||
/// <summary>
|
||||
/// The ExpressionExecutor dynamically compiles code and executes it using Roslyn.
|
||||
/// </summary>
|
||||
public class ExpressionExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// List of external type dependencies needed to compile expressions.
|
||||
/// </summary>
|
||||
private List<Type> dependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Script cache used to cache and re-use compiled Roslyn scripts.
|
||||
/// </summary>
|
||||
private ConcurrentDictionary<string, Script<object>> scriptCache;
|
||||
|
||||
/// <summary>
|
||||
/// Roslyn script options.
|
||||
/// </summary>
|
||||
private ScriptOptions scriptOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Global parameters passed to Roslyn scripts that can be referenced inside expressions.
|
||||
/// </summary>
|
||||
private CodeGenInputParams parameters;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the ExpressionExecutor class with objects that can be referenced in the schema.
|
||||
/// </summary>
|
||||
/// <param name="session">The tree session.</param>
|
||||
/// <param name="userContext">The dynamic user context.</param>
|
||||
/// <param name="dependencies">Type dependencies required to compile the schema. Can be null if no external dependencies required.</param>
|
||||
/// <param name="scriptCache">Script cache used to cache and re-use compiled Roslyn scripts.</param>
|
||||
public ExpressionExecutor(ITreeSession session, object userContext, List<Type> dependencies, ConcurrentDictionary<string, Script<object>> scriptCache)
|
||||
{
|
||||
this.dependencies = dependencies;
|
||||
this.parameters = new CodeGenInputParams
|
||||
{
|
||||
UserContext = userContext,
|
||||
Session = session
|
||||
};
|
||||
this.scriptCache = scriptCache ?? new ConcurrentDictionary<string, Script<object>>();
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the ExpressionExecutor class with objects that can be referenced in the schema.
|
||||
/// </summary>
|
||||
/// <param name="session">The tree session.</param>
|
||||
/// <param name="userContext">The dynamic user context.</param>
|
||||
/// <param name="dependencies">Type dependencies required to compile the schema. Can be null if no external dependencies required.</param>
|
||||
public ExpressionExecutor(ITreeSession session, object userContext, List<Type> dependencies)
|
||||
: this(session, userContext, dependencies, new ConcurrentDictionary<string, Script<object>>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the given expression and returns the result as the given generic type.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to evaluate.</param>
|
||||
/// <returns>The T value of the evaluated code.</returns>
|
||||
public async Task<T> Execute<T>(string expression)
|
||||
{
|
||||
var script = this.scriptCache.GetOrAdd(
|
||||
expression,
|
||||
(key) => CSharpScript.Create<object>(
|
||||
string.Format("return {0};", expression),
|
||||
this.scriptOptions,
|
||||
typeof(CodeGenInputParams)));
|
||||
|
||||
return (T)(await script.RunAsync(this.parameters).ConfigureAwait(false)).ReturnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Roslyn script options with all the required assemblies, references, and external dependencies.
|
||||
/// </summary>
|
||||
private void Initialize()
|
||||
{
|
||||
this.scriptOptions = ScriptOptions.Default;
|
||||
|
||||
// Add references to required assemblies.
|
||||
Assembly mscorlib = typeof(object).Assembly;
|
||||
Assembly cSharpAssembly = typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly;
|
||||
this.scriptOptions = this.scriptOptions.AddReferences(mscorlib, cSharpAssembly);
|
||||
|
||||
// Add required namespaces.
|
||||
this.scriptOptions = this.scriptOptions.AddImports("System");
|
||||
this.scriptOptions = this.scriptOptions.AddImports("System.Threading.Tasks");
|
||||
|
||||
string systemCoreAssemblyName = mscorlib.GetName().Name;
|
||||
|
||||
// Add external dependencies.
|
||||
if (this.dependencies != null)
|
||||
{
|
||||
foreach (Type type in this.dependencies)
|
||||
{
|
||||
string fullAssemblyName = type.Assembly.GetName().Name;
|
||||
|
||||
// While adding the reference again is okay, we can not AddImports for systemCoreAssembly.
|
||||
if (fullAssemblyName == systemCoreAssemblyName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this.scriptOptions = this.scriptOptions.AddReferences(type.Assembly).AddImports(type.Namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method used for testing if ScriptCache contains the expression key.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression key.</param>
|
||||
/// <returns>True if the expression key exists, otherwise false.</returns>
|
||||
internal bool ScriptCacheContainsKey(string expression)
|
||||
{
|
||||
return this.scriptCache.ContainsKey(expression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class defines the global parameter that will be
|
||||
/// passed into the Roslyn expression evaluator.
|
||||
/// </summary>
|
||||
public class CodeGenInputParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The dynamic UserContext object that holds properties and methods that can be referenced in the schema.
|
||||
/// </summary>
|
||||
public dynamic UserContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ITreeSession interface that holds accessor methods into the forgeState dictionary that can be referenced in the schema.
|
||||
/// </summary>
|
||||
public ITreeSession Session { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ForgeActionAttribute.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ForgeActionAttribute class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.Attributes
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// The ForgeActionAttribute class defines the ForgeAction attribute.
|
||||
///
|
||||
/// This attribute should be added to all Forge Action classes that wish to be called by Forge while walking the tree.
|
||||
/// The Name of the Action class will be used in the ForgeTree schema file to map to the Action with this ForgeAction attribute.
|
||||
/// Action classes must inherit from Forge's BaseAction class, as Forge will call the Setup and RunAction methods on these classes.
|
||||
///
|
||||
/// InputType should be included if the Action wishes for Forge to instantiate that object from the ForgeSchema and pass it to the Action.
|
||||
/// Otherwise, Forge will create a dynamic object from the Input in ForgeSchema if it exists, and pass it to the Action.
|
||||
///
|
||||
/// Ex) [ForgeAction(InputType: typeof(FooInput))]
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ForgeActionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// InputType should be included if the Action wishes for Forge to instantiate that object from the ForgeSchema and pass it to the Action.
|
||||
/// If null, Forge will create a dynamic object from the Input in ForgeSchema if it exists and pass it to the Action.
|
||||
/// </summary>
|
||||
public Type InputType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a ForgeActionAttribute.
|
||||
/// </summary>
|
||||
/// <param name="InputType">The input Type for this Action.</param>
|
||||
public ForgeActionAttribute(Type InputType)
|
||||
{
|
||||
this.InputType = InputType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a ForgeActionAttribute.
|
||||
/// </summary>
|
||||
public ForgeActionAttribute()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ForgeDictionary.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ForgeDictionary class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The ForgeDictionary class defines the methods for accessing forge state.
|
||||
/// Note: The KeyPrefix should always precede the key when using the forgeStateTable to limit the scope to the current SessionId.
|
||||
/// </summary>
|
||||
public class ForgeDictionary : IForgeDictionary
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this session.
|
||||
/// </summary>
|
||||
public Guid SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key prefix that should precede the keys when using the forgeStateTable.
|
||||
/// This ensures the scope of the table is limited to the current SessionId.
|
||||
/// </summary>
|
||||
private string keyPrefix;
|
||||
|
||||
/// <summary>
|
||||
/// The dictionary holding the forge state.
|
||||
/// Maps the string key to object value.
|
||||
/// The key should always be preceded by the KeyPrefix. This ensures the scope of the table is limited to the current SessionId.
|
||||
/// </summary>
|
||||
private IDictionary<string, object> forgeStateTable;
|
||||
|
||||
/// <summary>
|
||||
/// ForgeDictionary Constructor.
|
||||
/// </summary>
|
||||
/// <param name="forgeStateTable">The forge state table dictionary object.</param>
|
||||
/// <param name="sessionId">The unique identifier for this session.</param>
|
||||
public ForgeDictionary(IDictionary<string, object> forgeStateTable, Guid sessionId)
|
||||
{
|
||||
this.forgeStateTable = forgeStateTable;
|
||||
this.SessionId = sessionId;
|
||||
this.keyPrefix = this.SessionId + "_";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets an element with the provided key and value to the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to set.</param>
|
||||
/// <param name="value">The value of the element to be set.</param>
|
||||
public Task Set<T>(string key, T value)
|
||||
{
|
||||
this.forgeStateTable[this.keyPrefix + key] = (object)value;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a list of key value pairs to the backing store.
|
||||
/// </summary>
|
||||
/// <param name="kvps">The list of key value pairs to set.</param>
|
||||
public Task SetRange<T>(List<KeyValuePair<string, T>> kvps)
|
||||
{
|
||||
foreach(KeyValuePair<string, T> kvp in kvps)
|
||||
{
|
||||
this.forgeStateTable[this.keyPrefix + kvp.Key] = (object)kvp.Value;
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an element with the provided key from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to get.</param>
|
||||
/// <returns>The value of the element to get.</returns>
|
||||
public Task<T> GetValue<T>(string key)
|
||||
{
|
||||
return Task.FromResult((T)this.forgeStateTable[this.keyPrefix + key]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an element with the provided key from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to remove.</param>
|
||||
/// <returns>True of the element was removed, False otherwise.</returns>
|
||||
public Task<bool> RemoveKey(string key)
|
||||
{
|
||||
return Task.FromResult(this.forgeStateTable.Remove(this.keyPrefix + key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a list of elements with the provided keys from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="keys">The list of keys to remove.</param>
|
||||
public Task RemoveKeys(List<string> keys)
|
||||
{
|
||||
foreach(string key in keys)
|
||||
{
|
||||
this.forgeStateTable.Remove(this.keyPrefix + key);
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets an element with the provided key and value to the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to set.</param>
|
||||
/// <param name="value">The value of the element to be set.</param>
|
||||
public void SetSync<T>(string key, T value)
|
||||
{
|
||||
this.forgeStateTable[this.keyPrefix + key] = (object)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a list of key value pairs to the backing store.
|
||||
/// </summary>
|
||||
/// <param name="kvps">The list of key value pairs to set.</param>
|
||||
public void SetRangeSync<T>(List<KeyValuePair<string, T>> kvps)
|
||||
{
|
||||
foreach(KeyValuePair<string, T> kvp in kvps)
|
||||
{
|
||||
this.forgeStateTable[this.keyPrefix + kvp.Key] = (object)kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an element with the provided key from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to get.</param>
|
||||
/// <returns>The value of the element to get.</returns>
|
||||
public T GetValueSync<T>(string key)
|
||||
{
|
||||
return (T)this.forgeStateTable[this.keyPrefix + key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an element with the provided key from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to remove.</param>
|
||||
/// <returns>True of the element was removed, False otherwise.</returns>
|
||||
public bool RemoveKeySync(string key)
|
||||
{
|
||||
return this.forgeStateTable.Remove(this.keyPrefix + key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a list of elements with the provided keys from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="keys">The list of keys to remove.</param>
|
||||
public void RemoveKeysSync(List<string> keys)
|
||||
{
|
||||
foreach(string key in keys)
|
||||
{
|
||||
this.forgeStateTable.Remove(this.keyPrefix + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ForgeExceptions.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ForgeExceptions.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker.ForgeExceptions
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown on action timeout.
|
||||
/// </summary>
|
||||
public class ActionTimeoutException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActionTimeoutException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
public ActionTimeoutException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActionTimeoutException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public ActionTimeoutException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when ChildSelector fails to select any child.
|
||||
/// </summary>
|
||||
public class NoChildMatchedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NoChildMatchedException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
public NoChildMatchedException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when EvaluateDynamicProperty fails.
|
||||
/// </summary>
|
||||
public class EvaluateDynamicPropertyException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EvaluateDynamicPropertyException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
public EvaluateDynamicPropertyException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EvaluateDynamicPropertyException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public EvaluateDynamicPropertyException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IForgeDictionary.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The IForgeDictionary interface.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The IForgeDictionary interface defines the methods for accessing forge state.
|
||||
/// Note: The KeyPrefix should always precede the key when using the forgeStateTable to limit the scope to the current SessionId.
|
||||
/// </summary>
|
||||
public interface IForgeDictionary
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this session.
|
||||
/// </summary>
|
||||
Guid SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets an element with the provided key and value to the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to set.</param>
|
||||
/// <param name="value">The value of the element to be set.</param>
|
||||
Task Set<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a list of key value pairs to the backing store.
|
||||
/// </summary>
|
||||
/// <param name="kvps">The list of key value pairs to set.</param>
|
||||
Task SetRange<T>(List<KeyValuePair<string, T>> kvps);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an element with the provided key from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to get.</param>
|
||||
/// <returns>The value of the element to get.</returns>
|
||||
Task<T> GetValue<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an element with the provided key from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to remove.</param>
|
||||
/// <returns>True of the element was removed, False otherwise.</returns>
|
||||
Task<bool> RemoveKey(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a list of elements with the provided keys from the backing store.
|
||||
/// </summary>
|
||||
/// <param name="keys">The list of keys to remove.</param>
|
||||
Task RemoveKeys(List<string> keys);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ITreeSession.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ITreeSession interface.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The ITreeSession interface holds accessor methods into the forgeState dictionary.
|
||||
/// </summary>
|
||||
public interface ITreeSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the ActionResponse data from the forgeState for the given tree action key.
|
||||
/// </summary>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
|
||||
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
|
||||
ActionResponse GetOutput(string treeActionKey);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously gets the ActionResponse data from the forgeState for the given tree action key.
|
||||
/// </summary>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
|
||||
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
|
||||
Task<ActionResponse> GetOutputAsync(string treeActionKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last executed TreeAction's ActionResponse data from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
|
||||
ActionResponse GetLastActionResponse();
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously gets the last executed TreeAction's ActionResponse data from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
|
||||
Task<ActionResponse> GetLastActionResponseAsync();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ITreeWalkerCallbacks.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The ITreeWalkerCallbacks interface.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The ITreeWalkerCallbacks interface defines the callback Tasks that are awaited while walking the tree.
|
||||
/// </summary>
|
||||
public interface ITreeWalkerCallbacks
|
||||
{
|
||||
/// <summary>
|
||||
/// The callback Task that is awaited before visiting each node.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The Id of this tree walking session.</param>
|
||||
/// <param name="treeNodeKey">The key of the current tree node being visited by Forge.</param>
|
||||
/// <param name="properties">The additional properties for this node.</param>
|
||||
/// <param name="userContext">The dynamic user-defined context object.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
Task BeforeVisitNode(Guid sessionId, string treeNodeKey, dynamic properties, object userContext, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// The callback Task that is awaited after visiting each node.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The Id of this tree walking session.</param>
|
||||
/// <param name="treeNodeKey">The key of the current tree node being visited by Forge.</param>
|
||||
/// <param name="properties">The additional properties for this node.</param>
|
||||
/// <param name="userContext">The dynamic user-defined context object.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
Task AfterVisitNode(Guid sessionId, string treeNodeKey, dynamic properties, object userContext, CancellationToken token);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TreeWalkerParameters.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The TreeWalkerParameters class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
|
||||
/// <summary>
|
||||
/// The TreeWalkerParameters class contains the required and optional properties used by the TreeWalkerSession.
|
||||
/// </summary>
|
||||
public class TreeWalkerParameters
|
||||
{
|
||||
#region Required Properties
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier for this session.
|
||||
/// </summary>
|
||||
public Guid SessionId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The string representation of the JSON schema.
|
||||
/// </summary>
|
||||
public string JsonSchema { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The state given to TreeWalker on construction by a wrapper class.
|
||||
/// The state holds information that is relevant to TreeWalker while walking the tree.
|
||||
/// </summary>
|
||||
public IForgeDictionary ForgeState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ITreeWalkerCallbacks interface defines the callback Tasks that are awaited while walking the tree.
|
||||
/// </summary>
|
||||
public ITreeWalkerCallbacks Callbacks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token.
|
||||
/// </summary>
|
||||
public CancellationToken Token { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optional Properties
|
||||
|
||||
/// <summary>
|
||||
/// The dynamic object that is able to be referenced when evaluating schema expressions or performing actions.
|
||||
/// </summary>
|
||||
public object UserContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Assembly containing ForgeActionAttribute tagged classes.
|
||||
/// </summary>
|
||||
public Assembly ForgeActionsAssembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Script cache used by ExpressionExecutor to cache and re-use compiled Roslyn scripts.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<string, Script<object>> ScriptCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dependencies required to compile and execute the schema. Null if no external dependencies required.
|
||||
/// </summary>
|
||||
public List<Type> Dependencies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The external executors work similarly to the built-in ExpressionExecutor, but use their own string match and evaluation logic on schema expressions.
|
||||
/// The key is the string that EvaluateDynamicProperty will attempt to match string properties against. Similar to "C#|" for Roslyn expressions.
|
||||
/// The value is the Func that is called when the string key matches. This Func takes the expression and token, and should return the expected value type.
|
||||
/// </summary>
|
||||
public Dictionary<string, Func<string, CancellationToken, Task<object>>> ExternalExecutors { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a TreeWalkerParameters object with the properies that are required to instantiate a TreeWalkerSession object.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The unique identifier for this session.</param>
|
||||
/// <param name="jsonSchema">The JSON schema.</param>
|
||||
/// <param name="forgeState">The Forge state.</param>
|
||||
/// <param name="callbacks">The callbacks object.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
public TreeWalkerParameters(
|
||||
Guid sessionId,
|
||||
string jsonSchema,
|
||||
IForgeDictionary forgeState,
|
||||
ITreeWalkerCallbacks callbacks,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (sessionId == Guid.Empty) throw new ArgumentNullException("sessionId");
|
||||
if (string.IsNullOrWhiteSpace(jsonSchema)) throw new ArgumentNullException("jsonSchema");
|
||||
if (forgeState == null) throw new ArgumentNullException("forgeState");
|
||||
if (callbacks == null) throw new ArgumentNullException("callbacks");
|
||||
if (token == null) throw new ArgumentNullException("token");
|
||||
|
||||
this.SessionId = sessionId;
|
||||
this.JsonSchema = jsonSchema;
|
||||
this.ForgeState = forgeState;
|
||||
this.Callbacks = callbacks;
|
||||
this.Token = token;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,969 @@
|
|||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TreeWalkerSession.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
// <summary>
|
||||
// The TreeWalkerSession class.
|
||||
// </summary>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Forge.TreeWalker
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
|
||||
using Forge.Attributes;
|
||||
using Forge.DataContracts;
|
||||
using Forge.TreeWalker.ForgeExceptions;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// The TreeWalkerSession tries to walk the given tree schema to completion.
|
||||
/// It holds the logic for walking the tree schema, executing actions, and calling callbacks.
|
||||
/// </summary>
|
||||
public class TreeWalkerSession : ITreeSession
|
||||
{
|
||||
/// <summary>
|
||||
/// The ActionResponse suffix appended to the end of the key in forgeState that maps to an ActionResponse.
|
||||
/// Key: <SessionId>_<TreeActionKey>_AR
|
||||
/// </summary>
|
||||
public static string ActionResponseSuffix = "_AR";
|
||||
|
||||
/// <summary>
|
||||
/// The CurrentTreeNode suffix appended to the end of the key in forgeState that maps to the current TreeNode being walked.
|
||||
/// Key: <SessionId>_CTN
|
||||
/// </summary>
|
||||
public static string CurrentTreeNodeSuffix = "CTN";
|
||||
|
||||
/// <summary>
|
||||
/// The LastTreeAction suffix appended to the end of the key in forgeState that maps to the last TreeAction that was committed.
|
||||
/// Key: <SessionId>_LTA
|
||||
/// </summary>
|
||||
public static string LastTreeActionSuffix = "LTA";
|
||||
|
||||
/// <summary>
|
||||
/// The Intermediates suffix appended to the end of the key in forgeState that maps to an ActionContext's GetIntermediates object.
|
||||
/// Key: <SessionId>_<TreeActionKey>_Int
|
||||
/// </summary>
|
||||
public static string IntermediatesSuffix = "_Int";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the native LeafNodeSummaryAction.
|
||||
/// This Action can only live on Leaf type TreeNodes. Similarly, Leaf type TreeNodes can only have this Action.
|
||||
/// This Action takes an ActionResponse as the ActionInput, either as an object or as properties, and commits this object as the ActionResponse.
|
||||
/// This Action is intended to give schema authors the ability to cleanly end a tree walking path with a summary.
|
||||
/// </summary>
|
||||
public static string LeafNodeSummaryAction = "LeafNodeSummaryAction";
|
||||
|
||||
/// <summary>
|
||||
/// The Roslyn regex expression. Used to check if dynamic schema values should be evaluated with Roslyn.
|
||||
/// Type can be added to indicate that Roslyn should evaluate the expression and return the specified type.
|
||||
/// If the property is a "KnownType", the KnownType is used even if a <type> is specified in the expression. ActionDefinition.InputType is an example of a KnownType.
|
||||
/// If the property is not a KnownType and no type is specified in the expression, string is used by default.
|
||||
/// Ex) C#|"expression"
|
||||
/// Ex) C#<Boolean>|"expression"
|
||||
/// </summary>
|
||||
private static Regex RoslynRegex = new Regex(@"^C#(\<(\w+)\>)?\|");
|
||||
|
||||
/// <summary>
|
||||
/// The leading text to add to Schema strings to indicate the property value should be evaluated with Roslyn.
|
||||
/// The property value must match the RoslynRegex to be evaluated with Roslyn.
|
||||
/// </summary>
|
||||
private static string RoslynLeadingText = "C#";
|
||||
|
||||
/// <summary>
|
||||
/// The TreeWalkerParameters contains the required and optional properties used by the TreeWalkerSession.
|
||||
/// </summary>
|
||||
public TreeWalkerParameters Parameters { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON schema holding the tree to navigate during WalkTree.
|
||||
/// </summary>
|
||||
public ForgeTree Schema { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current status of the tree walker.
|
||||
/// </summary>
|
||||
public string Status { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The WalkTree cancellation token source.
|
||||
/// Used to send cancellation signal to action tasks and to stop tree walker from visiting future nodes.
|
||||
/// </summary>
|
||||
private CancellationTokenSource walkTreeCts;
|
||||
|
||||
/// <summary>
|
||||
/// The ExpressionExecutor dynamically compiles code and executes it.
|
||||
/// </summary>
|
||||
private ExpressionExecutor expressionExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// The map of string ActionNames to ActionDefinitions.
|
||||
/// This map is generated using reflection to find all the classes with the applied ForgeActionAttribute from the given Assembly.
|
||||
/// The string key is the Action class name.
|
||||
/// The ActionDefinition value contains the Action class type, and the InputType for the Action.
|
||||
/// </summary>
|
||||
private Dictionary<string, ActionDefinition> actionsMap;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a tree walker session with the required parameters.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The parameters object contains the required and optional properties used by the TreeWalkerSession.</param>
|
||||
public TreeWalkerSession(TreeWalkerParameters parameters)
|
||||
{
|
||||
if (parameters == null) throw new ArgumentNullException("parameters");
|
||||
|
||||
this.Parameters = parameters;
|
||||
|
||||
// Initialize properties from required TreeWalkerParameters properties.
|
||||
this.Schema = JsonConvert.DeserializeObject<ForgeTree>(parameters.JsonSchema);
|
||||
this.walkTreeCts = CancellationTokenSource.CreateLinkedTokenSource(parameters.Token);
|
||||
|
||||
// Initialize properties from optional TreeWalkerParameters properties.
|
||||
GetActionsMapFromAssembly(parameters.ForgeActionsAssembly, out this.actionsMap);
|
||||
parameters.ExternalExecutors = parameters.ExternalExecutors ?? new Dictionary<string, Func<string, CancellationToken, Task<object>>>();
|
||||
this.expressionExecutor = new ExpressionExecutor(this as ITreeSession, parameters.UserContext, parameters.Dependencies, parameters.ScriptCache);
|
||||
|
||||
this.Status = "Initialized";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ActionResponse data from the forgeState for the given tree action key.
|
||||
/// </summary>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
|
||||
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
|
||||
public ActionResponse GetOutput(string treeActionKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
return this.Parameters.ForgeState.GetValue<ActionResponse>(treeActionKey + ActionResponseSuffix).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously gets the ActionResponse data from the forgeState for the given tree action key.
|
||||
/// </summary>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
|
||||
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
|
||||
public async Task<ActionResponse> GetOutputAsync(string treeActionKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.Parameters.ForgeState.GetValue<ActionResponse>(treeActionKey + ActionResponseSuffix).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last executed TreeAction's ActionResponse data from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
|
||||
public ActionResponse GetLastActionResponse()
|
||||
{
|
||||
try
|
||||
{
|
||||
return this.Parameters.ForgeState.GetValue<ActionResponse>(this.GetLastTreeAction().GetAwaiter().GetResult() + ActionResponseSuffix).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously gets the last executed TreeAction's ActionResponse data from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
|
||||
public async Task<ActionResponse> GetLastActionResponseAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.Parameters.ForgeState.GetValue<ActionResponse>(await this.GetLastTreeAction().ConfigureAwait(false) + ActionResponseSuffix).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tree node being walked from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The current tree node if it has been persisted, otherwise null.</returns>
|
||||
public async Task<string> GetCurrentTreeNode()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.Parameters.ForgeState.GetValue<string>(CurrentTreeNodeSuffix).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last committed tree action from the forgeState.
|
||||
/// </summary>
|
||||
/// <returns>The last committed tree action if it has been persisted, otherwise null.</returns>
|
||||
public async Task<string> GetLastTreeAction()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.Parameters.ForgeState.GetValue<string>(LastTreeActionSuffix).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals the WalkTree and VisitNode cancellation token sources to cancel.
|
||||
/// </summary>
|
||||
public void CancelWalkTree()
|
||||
{
|
||||
this.walkTreeCts.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks the tree schema starting at the given tree node key.
|
||||
/// </summary>
|
||||
/// <param name="treeNodeKey">The TreeNode key to start walking.</param>
|
||||
/// <returns>The string status of the tree walker.</returns>
|
||||
public async Task<string> WalkTree(string treeNodeKey)
|
||||
{
|
||||
this.Status = "Running";
|
||||
|
||||
// Start a single task to walk the tree to completion.
|
||||
// This task will only start new tasks to execute actions.
|
||||
Task walkTreeTask = Task.Run(async () =>
|
||||
{
|
||||
string current = treeNodeKey;
|
||||
string next;
|
||||
|
||||
// Starting with the given tree node key, visit the returned children nodes until you hit a node with no matching children.
|
||||
// Call the callbacks before/after visiting each node.
|
||||
do
|
||||
{
|
||||
await this.CommitCurrentTreeNode(current).ConfigureAwait(false);
|
||||
if (this.walkTreeCts.Token.IsCancellationRequested)
|
||||
{
|
||||
this.Status = "Cancelled";
|
||||
this.walkTreeCts.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
await this.Parameters.Callbacks.BeforeVisitNode(
|
||||
this.Parameters.SessionId,
|
||||
current,
|
||||
await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null),
|
||||
this.Parameters.UserContext,
|
||||
this.walkTreeCts.Token).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// Exceptions are thrown here if VisitNode hit a timeout, was cancelled, or failed.
|
||||
next = await this.VisitNode(current).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always call this callback, whether or not visit node was successful.
|
||||
await this.Parameters.Callbacks.AfterVisitNode(
|
||||
this.Parameters.SessionId,
|
||||
current,
|
||||
await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null),
|
||||
this.Parameters.UserContext,
|
||||
this.walkTreeCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
current = next;
|
||||
} while (!string.IsNullOrWhiteSpace(current));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current))
|
||||
{
|
||||
// Null child means the tree ran to completion.
|
||||
this.Status = "RanToCompletion";
|
||||
}
|
||||
}, this.walkTreeCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
// Exceptions are thrown here if tree walker hit a timeout, was cancelled, or failed.
|
||||
// Let's update the Status according to the exception thrown before rethrowing the exception.
|
||||
await walkTreeTask;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Tree walker was cancelled before calling WalkTree.
|
||||
this.Status = "CancelledBeforeExecution";
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Tree walker was cancelled after calling WalkTree (no timeouts hit).
|
||||
this.Status = "Cancelled";
|
||||
}
|
||||
catch (ActionTimeoutException)
|
||||
{
|
||||
// An action-level timeout was hit.
|
||||
this.Status = "TimeoutOnAction";
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
// A node-level timeout was hit.
|
||||
this.Status = "TimeoutOnNode";
|
||||
}
|
||||
catch (NoChildMatchedException)
|
||||
{
|
||||
// ChildSelector couldn't select any child.
|
||||
this.Status = "RanToCompletion_NoChildMatched";
|
||||
}
|
||||
catch (EvaluateDynamicPropertyException)
|
||||
{
|
||||
this.Status = "Failed_EvaluateDynamicProperty";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// TODO: Consider checking the exception for specific Data entry and setting Status to that.
|
||||
// This would allow callbacks such as BeforeVisitNode to throw exceptions and control the status instead of it being Failed.
|
||||
// Unexpected exception was thrown in actions, callbacks, or elsewhere, resulting in failure and cancellation.
|
||||
this.Status = "Failed";
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.CancelWalkTree();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Exceptions are thrown here if tree walker hit a timeout, was cancelled, or failed.
|
||||
await walkTreeTask;
|
||||
}
|
||||
catch (NoChildMatchedException)
|
||||
{
|
||||
// For now, suppressing this exception so that its treated as successful end stage.
|
||||
}
|
||||
|
||||
return this.Status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a TreeNode in the ForgeTree, performing type-specific behavior as necessary before selecting the next child to visit.
|
||||
/// </summary>
|
||||
/// <param name="treeNodeKey">The TreeNode key to visit.</param>
|
||||
/// <exception cref="TimeoutException">If the node-level timeout was hit.</exception>
|
||||
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
|
||||
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
|
||||
/// <exception cref="Exception">If an unexpected exception was thrown.</exception>
|
||||
/// <returns>The key of the next child to visit, or <c>null</c> if no match was found.</returns>
|
||||
internal async Task<string> VisitNode(string treeNodeKey)
|
||||
{
|
||||
TreeNode treeNode = this.Schema.Tree[treeNodeKey];
|
||||
|
||||
// Do type-specific behavior.
|
||||
switch (treeNode.Type)
|
||||
{
|
||||
case TreeNodeType.Leaf:
|
||||
{
|
||||
await this.PerformLeafTypeBehavior(treeNode).ConfigureAwait(false);
|
||||
|
||||
// Leaf type can't have ChildSelector so we return here.
|
||||
return null;
|
||||
}
|
||||
case TreeNodeType.Action:
|
||||
{
|
||||
// Exceptions are thrown here if the actions hit a timeout, were cancelled, or failed.
|
||||
await this.PerformActionTypeBehavior(treeNode, treeNodeKey).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return next child to visit, if possible.
|
||||
return await this.SelectChild(treeNode).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the child selectors for a matching child.
|
||||
/// </summary>
|
||||
/// <param name="treeNode">The TreeNode to select a child from.</param>
|
||||
/// <returns>The key of the next child to visit, or <c>null</c> if no match was found.</returns>
|
||||
internal async Task<string> SelectChild(TreeNode treeNode)
|
||||
{
|
||||
if (treeNode.ChildSelector == null)
|
||||
{
|
||||
// No children to select, we are done walking the tree.
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (ChildSelector cs in treeNode.ChildSelector)
|
||||
{
|
||||
// Empty expressions default to true. Otherwise, evaluate the expression.
|
||||
if (string.IsNullOrWhiteSpace(cs.ShouldSelect) && !string.IsNullOrWhiteSpace(cs.Child))
|
||||
{
|
||||
return cs.Child;
|
||||
}
|
||||
if ((bool)await this.EvaluateDynamicProperty(cs.ShouldSelect, typeof(bool)).ConfigureAwait(false))
|
||||
{
|
||||
return cs.Child;
|
||||
}
|
||||
}
|
||||
|
||||
// No children were successfully matched.
|
||||
throw new NoChildMatchedException("ChildSelector couldn't match any child.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs Leaf TreeNodeType behavior.
|
||||
/// </summary>
|
||||
/// <param name="treeNode">The Leaf TreeNode.</param>
|
||||
internal async Task PerformLeafTypeBehavior(TreeNode treeNode)
|
||||
{
|
||||
// Check if Leaf node contains the LeafNodeSummaryAction and commit ActionInput as ActionResponse if it does.
|
||||
if (treeNode.Actions == null || treeNode.Actions.Count != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, TreeAction> kvp in treeNode.Actions)
|
||||
{
|
||||
string treeActionKey = kvp.Key;
|
||||
TreeAction treeAction = kvp.Value;
|
||||
|
||||
if (treeAction.Action != LeafNodeSummaryAction)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActionResponse actionResponse = await this.EvaluateDynamicProperty(treeAction.Input, typeof(ActionResponse)).ConfigureAwait(false);
|
||||
await this.CommitActionResponse(treeActionKey, actionResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the actions for the given tree node.
|
||||
/// Returns without throwing exception if all actions were completed successfully.
|
||||
/// </summary>
|
||||
/// <param name="treeNode">The TreeNode containing actions to execute.</param>
|
||||
/// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
|
||||
/// <exception cref="TimeoutException">If the node-level timeout was hit.</exception>
|
||||
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
|
||||
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
|
||||
internal async Task PerformActionTypeBehavior(TreeNode treeNode, string treeNodeKey)
|
||||
{
|
||||
List<Task> actionTasks = new List<Task>();
|
||||
|
||||
if (treeNode.Actions == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Start new parallel tasks for each action on this node.
|
||||
foreach (KeyValuePair<string, TreeAction> kvp in treeNode.Actions)
|
||||
{
|
||||
string treeActionKey = kvp.Key;
|
||||
TreeAction treeAction = kvp.Value;
|
||||
|
||||
if (await this.GetOutputAsync(treeActionKey).ConfigureAwait(false) != null)
|
||||
{
|
||||
// Handle rehydration case. Commit LastTreeAction if it was not committed. Do not execute actions for which we have already received a response.
|
||||
if (await this.GetLastTreeAction().ConfigureAwait(false) == null)
|
||||
{
|
||||
await this.CommitLastTreeAction(treeActionKey).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.actionsMap.TryGetValue(treeAction.Action, out ActionDefinition actionDefinition))
|
||||
{
|
||||
actionTasks.Add(this.ExecuteActionWithRetry(treeNodeKey, treeActionKey, treeAction, actionDefinition, this.walkTreeCts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all parallel tasks to complete until the given timout.
|
||||
// If any task hits a timeout, gets cancelled, or fails, an exception will be thrown.
|
||||
// Note: CancelWalkTree is called at the end of every session to ensure all Actions/Tasks see the triggered cancellation token.
|
||||
Task nodeTimeoutTask = Task.Delay((int)await this.EvaluateDynamicProperty(treeNode.Timeout ?? -1, typeof(int)).ConfigureAwait(false), this.walkTreeCts.Token);
|
||||
actionTasks.Add(nodeTimeoutTask);
|
||||
|
||||
while (actionTasks.Count > 1)
|
||||
{
|
||||
// Throw if cancellation was requested between actions completing.
|
||||
this.walkTreeCts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
Task completedTask = await Task.WhenAny(actionTasks).ConfigureAwait(false);
|
||||
actionTasks.Remove(completedTask);
|
||||
|
||||
if (completedTask == nodeTimeoutTask)
|
||||
{
|
||||
// Throw on cancellation requested if that's the reason the timeout task completed.
|
||||
this.walkTreeCts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
// NodeTimeout was hit, throw a special exception to differentiate between timeout and cancellation.
|
||||
throw new TimeoutException("Hit node-level timeout in TreeNodeKey: " + treeNodeKey);
|
||||
}
|
||||
|
||||
// Await the completed task to propagate any exceptions.
|
||||
// Exceptions are thrown here if the action hit a timeout, was cancelled, or failed.
|
||||
await completedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the given action. Attempts retries according to the retry policy and timeout.
|
||||
/// Returns without throwing exception if the action was completed successfully.
|
||||
/// </summary>
|
||||
/// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action taking place.</param>
|
||||
/// <param name="treeAction">The TreeAction object that holds properties of the action.</param>
|
||||
/// <param name="actionDefinition">The object holding definitions for the action to execute.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
|
||||
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
|
||||
internal async Task ExecuteActionWithRetry(
|
||||
string treeNodeKey,
|
||||
string treeActionKey,
|
||||
TreeAction treeAction,
|
||||
ActionDefinition actionDefinition,
|
||||
CancellationToken token)
|
||||
{
|
||||
// Initialize values. Default infinite timeout. Default RetryPolicyType.None.
|
||||
int retryCount = 0;
|
||||
Exception innerException = null;
|
||||
Stopwatch stopwatch = new Stopwatch();
|
||||
|
||||
int actionTimeout = (int)await this.EvaluateDynamicProperty(treeAction.Timeout ?? -1, typeof(int)).ConfigureAwait(false);
|
||||
RetryPolicyType retryPolicyType = treeAction.RetryPolicy != null ? treeAction.RetryPolicy.Type : RetryPolicyType.None;
|
||||
TimeSpan waitTime = treeAction.RetryPolicy != null ? TimeSpan.FromMilliseconds(treeAction.RetryPolicy.MinBackoffMs) : new TimeSpan();
|
||||
|
||||
// Kick off timers.
|
||||
Task actionTimeoutTask = Task.Delay(actionTimeout, token);
|
||||
stopwatch.Start();
|
||||
|
||||
// Attmpt to ExecuteAction based on RetryPolicy and Timeout.
|
||||
// Throw on non-retriable exceptions.
|
||||
while (actionTimeout == -1 || stopwatch.ElapsedMilliseconds < actionTimeout)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await this.ExecuteAction(treeNodeKey, treeActionKey, treeAction, actionDefinition, actionTimeoutTask, token).ConfigureAwait(false);
|
||||
return; // success!
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw; // non-retriable exception
|
||||
}
|
||||
catch (ActionTimeoutException)
|
||||
{
|
||||
throw; // non-retriable exception
|
||||
}
|
||||
catch (EvaluateDynamicPropertyException)
|
||||
{
|
||||
throw; // non-retriable exception
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Cache exception as innerException in case we need to throw ActionTimeoutException.
|
||||
innerException = e;
|
||||
|
||||
// Hit retriable exception. Retry according to RetryPolicy.
|
||||
// When retries are exhausted, throw ActionTimeoutException with Exception e as the innerException.
|
||||
switch (retryPolicyType)
|
||||
{
|
||||
case RetryPolicyType.FixedInterval:
|
||||
{
|
||||
// FixedInterval retries every MinBackoffMs until the timeout.
|
||||
// Ex) 200ms, 200ms, 200ms...
|
||||
waitTime = TimeSpan.FromMilliseconds(treeAction.RetryPolicy.MinBackoffMs);
|
||||
break;
|
||||
}
|
||||
case RetryPolicyType.ExponentialBackoff:
|
||||
{
|
||||
// ExponentialBackoff retries every Math.Min(MinBackoffMs * 2^(retryCount), MaxBackoffMs) until the timeout.
|
||||
// Ex) 100ms, 200ms, 400ms...
|
||||
waitTime = TimeSpan.FromMilliseconds(Math.Min(treeAction.RetryPolicy.MaxBackoffMs, waitTime.TotalMilliseconds * 2));
|
||||
break;
|
||||
}
|
||||
case RetryPolicyType.None:
|
||||
default:
|
||||
{
|
||||
// No retries. Break out below to throw non-retriable exception.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Break out if no retry policy set.
|
||||
if (retryPolicyType == RetryPolicyType.None)
|
||||
{
|
||||
// If the retries have exhausted and the ContinuationOnRetryExhaustion flag is set, commit a new ActionResponse
|
||||
// with the status set to RetryExhaustedOnAction and return.
|
||||
if (treeAction.ContinuationOnRetryExhaustion)
|
||||
{
|
||||
ActionResponse timeoutResponse = new ActionResponse
|
||||
{
|
||||
Status = "RetryExhaustedOnAction"
|
||||
};
|
||||
|
||||
await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Break out early if we would hit timeout before next retry.
|
||||
if (actionTimeout != -1 && stopwatch.ElapsedMilliseconds + waitTime.TotalMilliseconds >= actionTimeout)
|
||||
{
|
||||
// If the timeout is hit and the ContinuationOnTimeout flag is set, commit a new ActionResponse
|
||||
// with the status set to TimeoutOnAction and return.
|
||||
if (treeAction.ContinuationOnTimeout)
|
||||
{
|
||||
ActionResponse timeoutResponse = new ActionResponse
|
||||
{
|
||||
Status = "TimeoutOnAction"
|
||||
};
|
||||
|
||||
await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
await Task.Delay(waitTime, token).ConfigureAwait(false);
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
// Retries are exhausted. Throw ActionTimeoutException with executeAction exception as innerException.
|
||||
throw new ActionTimeoutException(
|
||||
string.Format(
|
||||
"Action did not complete successfully. TreeNodeKey: {0}, TreeActionKey: {1}, ActionName: {2}, RetryCount: {3}, RetryPolicy: {4}",
|
||||
treeNodeKey,
|
||||
treeActionKey,
|
||||
treeAction.Action,
|
||||
retryCount,
|
||||
retryPolicyType),
|
||||
innerException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the given actionTask and commits the ActionResponse to forgeState on success.
|
||||
/// </summary>
|
||||
/// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action taking place.</param>
|
||||
/// <param name="treeAction">The TreeAction object that holds properties of the action.</param>
|
||||
/// <param name="actionDefinition">The object holding definitions for the action to execute.</param>
|
||||
/// <param name="actionTimeoutTask">The delay task tied to the action timeout.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
|
||||
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
|
||||
/// <returns>
|
||||
/// RanToCompletion if the action was completed successfully.
|
||||
/// Exceptions are thrown on timeout, cancellation, or retriable failures.
|
||||
/// </returns>
|
||||
internal async Task ExecuteAction(
|
||||
string treeNodeKey,
|
||||
string treeActionKey,
|
||||
TreeAction treeAction,
|
||||
ActionDefinition actionDefinition,
|
||||
Task actionTimeoutTask,
|
||||
CancellationToken token)
|
||||
{
|
||||
// Set up a linked cancellation token to trigger on timeout if ContinuationOnTimeout is set.
|
||||
// This ensures the runActionTask gets canceled when Forge timeout is hit.
|
||||
CancellationTokenSource actionCts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
token = treeAction.ContinuationOnTimeout ? actionCts.Token : token;
|
||||
|
||||
// Evaluate the dynamic properties that are used by the actionTask.
|
||||
ActionContext actionContext = new ActionContext(
|
||||
this.Parameters.SessionId,
|
||||
treeNodeKey,
|
||||
treeActionKey,
|
||||
treeAction.Action,
|
||||
await this.EvaluateDynamicProperty(treeAction.Input, actionDefinition.InputType).ConfigureAwait(false),
|
||||
await this.EvaluateDynamicProperty(treeAction.Properties, null).ConfigureAwait(false),
|
||||
this.Parameters.UserContext,
|
||||
token,
|
||||
this.Parameters.ForgeState
|
||||
);
|
||||
|
||||
// Instantiate the BaseAction-derived ActionType class and invoke the RunAction method on it.
|
||||
var actionObject = Activator.CreateInstance(actionDefinition.ActionType);
|
||||
MethodInfo method = typeof(BaseAction).GetMethod("RunAction");
|
||||
Task<ActionResponse> runActionTask = (Task<ActionResponse>) method.Invoke(actionObject, new object[] { actionContext });
|
||||
|
||||
// Await for the first completed task between our runActionTask and the timeout task.
|
||||
// This allows us to continue without awaiting the runActionTask upon timeout.
|
||||
var completedTask = await Task.WhenAny(runActionTask, actionTimeoutTask).ConfigureAwait(false);
|
||||
|
||||
if (completedTask == actionTimeoutTask)
|
||||
{
|
||||
// Throw on cancellation requested if that's the reason the timeout task completed.
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// If the timeout is hit and the ContinuationOnTimeout flag is set, commit a new ActionResponse
|
||||
// with the status set to TimeoutOnAction and return.
|
||||
if (treeAction.ContinuationOnTimeout)
|
||||
{
|
||||
// Trigger linked cancellation token before continuing to ensure the runActionTask gets cancelled.
|
||||
actionCts.Cancel();
|
||||
|
||||
ActionResponse timeoutResponse = new ActionResponse
|
||||
{
|
||||
Status = "TimeoutOnAction"
|
||||
};
|
||||
|
||||
await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// ActionTimeout has been hit. Throw special exception to indicate this.
|
||||
throw new ActionTimeoutException(string.Format(
|
||||
"ActionTimeoutTask timed out before Action could complete. TreeNodeKey: {0}, TreeActionKey: {1}, ActionName: {2}.",
|
||||
treeNodeKey,
|
||||
treeActionKey,
|
||||
treeAction.Action));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle the completed runActionTask.
|
||||
if (runActionTask.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
await this.CommitActionResponse(treeActionKey, await runActionTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Await the completed task to propagate any exceptions.
|
||||
// Exceptions are thrown here if the action hit a timeout, was cancelled, or failed.
|
||||
await runActionTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates through the given schema object, evaluating any Roslyn expressions found in the values.
|
||||
/// When a knownType is given, Forge will instantiate that type instead of a dynamic object.
|
||||
/// String properties matching the <see cref="RoslynRegex"/> represent a code-snippet that will be evaluated.
|
||||
/// </summary>
|
||||
/// <param name="schemaObj">The object given from the schema.</param>
|
||||
/// <param name="knownType">The type of the object being evaluated. Null here represents an unknown type that will be evaluated dynamically.</param>
|
||||
/// <exception cref="EvaluateDynamicPropertyException">If exceptions are thrown while evaluating the dynamic property.</exception>
|
||||
/// <returns>The properties after evaluation.</returns>
|
||||
public async Task<object> EvaluateDynamicProperty(dynamic schemaObj, Type knownType)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (schemaObj == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (schemaObj is string && schemaObj.StartsWith(RoslynLeadingText))
|
||||
{
|
||||
// Case when schema property is a Roslyn expression.
|
||||
// Evaluate it as either the knownType if it exists, the <type> embeded in the RoslynRegex, or a string by default, in that order.
|
||||
Match result = RoslynRegex.Match(schemaObj);
|
||||
if (result.Success)
|
||||
{
|
||||
string typeStr = string.IsNullOrWhiteSpace(result.Groups[2].Value) ? "String" : result.Groups[2].Value;
|
||||
Type type = knownType ?? Type.GetType("System." + typeStr);
|
||||
|
||||
MethodInfo method = typeof(ExpressionExecutor).GetMethod("Execute");
|
||||
MethodInfo genericMethod = method.MakeGenericMethod(type);
|
||||
string expression = schemaObj.Substring(result.Groups[0].Value.Length);
|
||||
var res = await (dynamic) genericMethod.Invoke(this.expressionExecutor, new object[] { expression });
|
||||
return res;
|
||||
}
|
||||
}
|
||||
else if (schemaObj is string)
|
||||
{
|
||||
foreach (var kvp in this.Parameters.ExternalExecutors)
|
||||
{
|
||||
string regexString = kvp.Key;
|
||||
Func<string, CancellationToken, Task<object>> externalExecutor = kvp.Value;
|
||||
|
||||
if (schemaObj.StartsWith(regexString))
|
||||
{
|
||||
// Case when schema property matches an external executor.
|
||||
// Evaluate it and return the result as the knownType if it exists or as a string otherwise.
|
||||
var result = await externalExecutor(schemaObj.Substring(regexString.Length), this.walkTreeCts.Token).ConfigureAwait(false);
|
||||
return knownType != null ? Convert.ChangeType(result, knownType) : result;
|
||||
}
|
||||
}
|
||||
|
||||
return schemaObj;
|
||||
}
|
||||
else if (schemaObj is JObject)
|
||||
{
|
||||
// Case when schema object has properties (i.e. is a dictionary).
|
||||
// Instantiate and use the knownType if given, then evaluate each property using recursion.
|
||||
dynamic knownObj = Activator.CreateInstance(knownType ?? typeof(object));
|
||||
|
||||
IDictionary<string, dynamic> propertyValues = schemaObj.ToObject<IDictionary<string, dynamic>>();
|
||||
foreach (string key in new List<string>(propertyValues.Keys))
|
||||
{
|
||||
if (knownType != null)
|
||||
{
|
||||
var prop = knownType.GetProperty(key);
|
||||
prop.SetValue(knownObj, await this.EvaluateDynamicProperty(propertyValues[key], prop.PropertyType).ConfigureAwait(false));
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyValues[key] = await this.EvaluateDynamicProperty(propertyValues[key], null).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return knownType != null ? knownObj : (dynamic)JObject.FromObject(propertyValues);
|
||||
}
|
||||
else if (schemaObj is JArray)
|
||||
{
|
||||
// Case when schema object is an array.
|
||||
// Create an array with the knownType if given, then use recursion to evaluate each index.
|
||||
dynamic knownObj = Activator.CreateInstance(knownType ?? typeof(object[]), schemaObj.Count);
|
||||
|
||||
for (int i = 0; i < schemaObj.Count; i++)
|
||||
{
|
||||
if (knownType != null)
|
||||
{
|
||||
knownObj.SetValue(await this.EvaluateDynamicProperty(schemaObj[i], knownType.GetElementType()).ConfigureAwait(false), i);
|
||||
}
|
||||
else
|
||||
{
|
||||
schemaObj[i] = await this.EvaluateDynamicProperty(schemaObj[i], null).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return knownType != null ? knownObj : schemaObj;
|
||||
}
|
||||
else if (schemaObj is JValue)
|
||||
{
|
||||
// Case when schema object is a value type.
|
||||
// Return the value as the knownType if given.
|
||||
return knownType != null ? Convert.ChangeType(schemaObj.Value, knownType) : schemaObj.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case when schema object is a value type.
|
||||
// Return the value as the knownType if given.
|
||||
return knownType != null ? Convert.ChangeType(schemaObj, knownType) : schemaObj;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw; // rethrow on cancelled.
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new EvaluateDynamicPropertyException(
|
||||
string.Format("EvaluateDynamicProperty failed to parse schemaObj: {0}, knownType: {1}.", schemaObj?.ToString(), knownType),
|
||||
e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits the ActionResponse to the forgeState.
|
||||
/// This allows the ActionResponses to be dynamically referenced in the ForgeTree through ITreeSession interface.
|
||||
/// </summary>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
|
||||
/// <param name="actionResponse">The action response object returned from the action.</param>
|
||||
private async Task CommitActionResponse(string treeActionKey, ActionResponse actionResponse)
|
||||
{
|
||||
await this.Parameters.ForgeState.Set<ActionResponse>(treeActionKey + ActionResponseSuffix, actionResponse).ConfigureAwait(false);
|
||||
await this.CommitLastTreeAction(treeActionKey).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits the current tree node to the forgeState.
|
||||
/// The wrapper class has access to this state, allowing it to rehydrate/retry on failures if desired.
|
||||
/// </summary>
|
||||
/// <param name="treeNodeKey">The TreeNode's key that tree walker is currently walking.</param>
|
||||
private Task CommitCurrentTreeNode(string treeNodeKey)
|
||||
{
|
||||
// TODO: Consider adding a "nodeStep" to save if we already completed the BeforeVisitNode step and should skip straight to VisitNode.
|
||||
return this.Parameters.ForgeState.Set<string>(CurrentTreeNodeSuffix, treeNodeKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits the last tree action to the forgeState.
|
||||
/// </summary>
|
||||
/// <param name="treeActionKey">The TreeAction's key of the last action that was committed.</param>
|
||||
private Task CommitLastTreeAction(string treeActionKey)
|
||||
{
|
||||
return this.Parameters.ForgeState.Set<string>(LastTreeActionSuffix, treeActionKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the actionsMap from the given assembly.
|
||||
/// This map is generated using reflection to find all the classes with the applied ForgeActionAttribute from the given Assembly.
|
||||
/// </summary>
|
||||
/// <param name="forgeActionsAssembly">The Assembly containing ForgeActionAttribute tagged classes.</param>
|
||||
/// <param name="actionsMap">The map of string ActionNames to ActionDefinitions.</param>
|
||||
public static void GetActionsMapFromAssembly(Assembly forgeActionsAssembly, out Dictionary<string, ActionDefinition> actionsMap)
|
||||
{
|
||||
actionsMap = new Dictionary<string, ActionDefinition>();
|
||||
|
||||
if (forgeActionsAssembly == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Type type in forgeActionsAssembly.GetExportedTypes())
|
||||
{
|
||||
// Find all classes with the applied ForgeActionAttribute.
|
||||
ForgeActionAttribute forgeAction = (ForgeActionAttribute) type.GetCustomAttribute(typeof(ForgeActionAttribute), false);
|
||||
if (forgeAction != null)
|
||||
{
|
||||
// Confirm that ForgeActionAttribute is attached to only BaseAction classes and there are no duplicates.
|
||||
Type derived = type;
|
||||
bool isBaseAction = false;
|
||||
do
|
||||
{
|
||||
if (derived == typeof(BaseAction))
|
||||
{
|
||||
isBaseAction = true;
|
||||
break;
|
||||
}
|
||||
|
||||
derived = derived.BaseType;
|
||||
} while (derived != null);
|
||||
|
||||
if (isBaseAction)
|
||||
{
|
||||
actionsMap.Add(type.Name, new ActionDefinition() { ActionType = type, InputType = forgeAction.InputType });
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CustomAttributeFormatException(
|
||||
string.Format(
|
||||
"The given type: {0} must implement the BaseAction abstract class in order to apply the ForgeActionAttribute.",
|
||||
type.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29215.179
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Forge.TreeWalker", "Forge.TreeWalker\Forge.TreeWalker.csproj", "{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Forge.DataContracts", "Forge.DataContracts\Forge.DataContracts.csproj", "{C49A8494-13E5-4214-8434-708BA280C5B1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Forge.TreeWalker.UnitTests", "Forge.TreeWalker.UnitTests\Forge.TreeWalker.UnitTests.csproj", "{A33AF1FF-1291-4CB1-A733-3243E1FE967E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C49A8494-13E5-4214-8434-708BA280C5B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C49A8494-13E5-4214-8434-708BA280C5B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C49A8494-13E5-4214-8434-708BA280C5B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C49A8494-13E5-4214-8434-708BA280C5B1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A8067EC1-B359-4193-8603-5E6524B2B3A1}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
Загрузка…
Ссылка в новой задаче