// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using OpenTap.Addin; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml.Serialization; namespace OpenTap { partial class TestPlan { bool runPrePlanRunMethods(IList steps, TestPlanRun planRun) { Stopwatch preTimer = Stopwatch.StartNew(); // try to avoid calling Stopwatch.StartNew too often. TimeSpan elaps = preTimer.Elapsed; foreach (ITestStep step in steps) { if (step.Enabled == false) continue; bool runPre = true; if (step is TestStep s) { runPre = s.PrePostPlanRunUsed; } planRun.StepsWithPrePlanRun.Add(step); try { if (runPre) { planRun.AddTestStepStateUpdate(step.Id, null, StepState.PrePlanRun); try { step.PlanRun = planRun; planRun.ResourceManager.BeginStep(planRun, step, TestPlanExecutionStage.PrePlanRun, TapThread.Current.AbortToken); try { step.PrePlanRun(); } finally { planRun.ResourceManager.EndStep(step, TestPlanExecutionStage.PrePlanRun); step.PlanRun = null; } } finally { planRun.AddTestStepStateUpdate(step.Id, null, StepState.Idle); } } if (!runPrePlanRunMethods(step.ChildTestSteps, planRun)) { return false; } } catch (Exception ex) { Log.Error(String.Format("PrePlanRun of '{0}' failed with message '{1}'.", step.Name, ex.Message)); Log.Debug(ex); Log.Error("Aborting TestPlan."); return false; } finally { if (runPre) { var newelaps = preTimer.Elapsed; Log.Debug(newelaps - elaps, "{0} PrePlanRun completed.", step.GetStepPath()); elaps = newelaps; } } } return true; } #region Nested Types enum failState { Ok, StartFail, ExecFail } #endregion internal static void PrintWaitingMessage(IEnumerable resources) { // Save disconnected ressources to avoid race conditions. var waitingRessources = resources.Where(r => !r.IsConnected).ToArray(); if (waitingRessources.Length == 0) { return; } Log.Info("Waiting for resources to open:"); foreach (var resource in waitingRessources) { Log.Info(" - {0}", resource); } } failState execTestPlan(TestPlanRun execStage, IList steps) { WaitHandle.WaitAny(new[] { execStage.PromptWaitHandle, TapThread.Current.AbortToken.WaitHandle }); bool resultListenerError = false; execStage.ScheduleInResultProcessingThread(resultListener => { try { using (TimeoutOperation.Create(() => PrintWaitingMessage(new List() { resultListener }))) execStage.ResourceManager.WaitUntilResourcesOpened(TapThread.Current.AbortToken, resultListener); try { // some resources might set metadata in the Open methods. // this information needs to be propagated to result listeners as well. // this returns quickly if its a lazy resource manager. using (TimeoutOperation.Create( () => PrintWaitingMessage(new List() { resultListener }))) execStage.ResourceManager.WaitUntilAllResourcesOpened(TapThread.Current.AbortToken); } catch { // this error will also be handled somewhere else. } execStage.WaitForSerialization(); foreach (var res in execStage.PromptedResources) execStage.Parameters.AddRange(ResultParameters.GetMetadataFromObject(res)); resultListener.OnTestPlanRunStart(execStage); } catch (OperationCanceledException) when (execStage.MainThread.AbortToken.IsCancellationRequested) { // test plan thread was aborted, this is OK. } catch (Exception ex) { Log.Error("Error in OnTestPlanRunStart for '{0}': '{1}'", resultListener, ex.Message); Log.Debug(ex); resultListenerError = true; } }, true); if (resultListenerError) return failState.StartFail; var sw = Stopwatch.StartNew(); try { execStage.StepsWithPrePlanRun.Clear(); // Invoke test plan pre run event mixins. TestPlanPreRunEvent.Invoke(this); if (!runPrePlanRunMethods(steps, execStage)) { return failState.StartFail; } } catch (Exception e) { Log.Error(e.GetInnerMostExceptionMessage()); Log.Debug(e); return failState.StartFail; } finally { { Log.Debug(sw, "PrePlanRun Methods completed"); } } Stopwatch planRunOnlyTimer = Stopwatch.StartNew(); var runs = new List(); bool didBreak = false; void addBreakResult(TestStepRun run) { if (didBreak) return; didBreak = true; execStage.Parameters.Add(new ResultParameter(TestPlanRun.SpecialParameterNames.BreakIssuedFrom, run.Id.ToString())); } TestStepRun getBreakingRun(TestStepRun first) { while ((first.Exception as TestStepBreakException)?.Run is { } inner) first = inner; return first; } try { for (int i = 0; i < steps.Count; i++) { var step = steps[i]; if (step.Enabled == false) continue; var run = step.DoRun(execStage, execStage); if (!run.Skipped) runs.Add(run); if (run.BreakConditionsSatisfied()) { var breakingRun = getBreakingRun(run); addBreakResult(breakingRun); run.LogBreakCondition(); break; } // note: The following is copied inside TestStep.cs if (run.SuggestedNextStep is Guid id) { int nextindex = steps.IndexWhen(x => x.Id == id); if (nextindex >= 0) i = nextindex - 1; // if skip to next step, dont add it to the wait queue. } } } catch (TestStepBreakException breakEx) { var breakingRun = getBreakingRun(breakEx.Run); addBreakResult(breakingRun); Log.Info("{0}", breakEx.Message); } finally { // Now wait for them to actually complete. They might defer internally. foreach (var run in runs) { run.WaitForCompletion(); execStage.UpgradeVerdict(run.Verdict); } } Log.Debug(planRunOnlyTimer, "Test step runs finished."); return failState.Ok; } void finishTestPlanRun(TestPlanRun run, Stopwatch testPlanTimer, failState runWentOk, TraceListener Logger, HybridStream logStream) { try { if (run != null) { if (runWentOk == failState.StartFail) { if (PrintTestPlanRunSummary) summaryListener.OnTestPlanRunStart(run); // Call this to ensure that the correct planrun is being summarized if (run.Verdict < Verdict.Aborted) run.Verdict = Verdict.Error; } for (int i = run.StepsWithPrePlanRun.Count - 1; i >= 0; i--) { Stopwatch postTimer = Stopwatch.StartNew(); String stepPath = string.Empty; try { ITestStep step = run.StepsWithPrePlanRun[i]; if ((step as TestStep)?.PrePostPlanRunUsed ?? true) { stepPath = step.GetStepPath(); run.AddTestStepStateUpdate(step.Id, null, StepState.PostPlanRun); try { run.ResourceManager.BeginStep(run, step, TestPlanExecutionStage.PostPlanRun, TapThread.Current.AbortToken); step.PlanRun = run; try { step.PostPlanRun(); } finally { run.ResourceManager.EndStep(step, TestPlanExecutionStage.PostPlanRun); step.PlanRun = null; } } finally { run.AddTestStepStateUpdate(step.Id, null, StepState.Idle); } Log.Debug(postTimer, "{0} PostPlanRun completed.", stepPath); } } catch (Exception ex) { Log.Warning("Error during post plan run of {0}.", stepPath); Log.Debug(ex); } } run.Duration = testPlanTimer.Elapsed; } if (run != null) { try { // The open resource threads might throw exceptions. If they do we must // Wait() for them to catch the exception. // If the run was aborted after the open resource threads were started but // before we wait for them (e.g. by an error in PrePlanRun), then we do it // here. run.ResourceManager.WaitUntilAllResourcesOpened(TapThread.Current.AbortToken); } catch (OperationCanceledException) { // Ignore this because this typically means that the wait was cancelled before. // Just to be sure also upgrade verdict to aborted. run.UpgradeVerdict(Verdict.Aborted); } catch (Exception e) { Log.Error("Failed to open resource ({0})", e.GetInnerMostExceptionMessage()); Log.Debug(e); } if (PrintTestPlanRunSummary) { // wait for the summaryListener so the summary appears in the log file. run.WaitForResultListener(summaryListener); summaryListener.PrintSummary(); } OpenTap.Log.Flush(); Logger.Flush(); logStream.Flush(); run.AddTestPlanCompleted(logStream, runWentOk != failState.StartFail); if (PrintTestPlanRunSummary) summaryListener.PrintArtifactsSummary(); run.ResourceManager.EndStep(this, TestPlanExecutionStage.Execute); if (!run.IsCompositeRun) run.ResourceManager.EndStep(this, TestPlanExecutionStage.Open); } } finally { if (monitors != null) foreach (var item in monitors) item.ExitTestPlanRun(run); } } /// When true, prints the test plan run summary at the end of a run. [XmlIgnore] [AnnotationIgnore] public bool PrintTestPlanRunSummary { get; set; } /// /// FileGlobals±äÁ¿ /// public ObservableCollection ParameterVariables { get; set; } = new ObservableCollection(); public ObservableCollection Variables { get; set; } = new ObservableCollection(); /// /// Calls the PromptForDutMetadata delegate for all referenced DUTs. /// internal void StartResourcePromptAsync(TestPlanRun planRun, IEnumerable _resources) { var resources = _resources.Where(x => x != null).ToArray(); var componentSettingsWithMetaData = new List(); var componentSettings = TypeData.GetDerivedTypes() .Where(type => type.CanCreateInstance); bool anyMetaData = false; planRun.PromptWaitHandle.Reset(); try { foreach (var setting in componentSettings) { foreach (var member in setting.GetMembers()) { var attr = member.GetAttribute(); if (attr != null && attr.PromptUser) { anyMetaData = true; componentSettingsWithMetaData.Add(setting); break; // avoid adding this multiple times. } } } foreach (var resource in resources) { if (anyMetaData) break; var type = TypeData.GetTypeData(resource); foreach (var prop in type.GetMembers()) { var attr = prop.GetAttribute(); if (attr != null && attr.PromptUser) { anyMetaData = true; break; } } } } catch { // this is just a defensive catch to make sure that the WaitHandle is not left unset (and we risk waiting for it indefinitely) planRun.PromptWaitHandle.Set(); throw; } if (anyMetaData && EngineSettings.Current.PromptForMetaData) { TapThread.Start(() => { try { List objects = new List(); objects.AddRange(componentSettingsWithMetaData.Select(ComponentSettings.GetCurrent)); objects.AddRange(resources); objects.RemoveIf(x => x == null); //ComponentSettings.GetCurrent can return null for abstract types. planRun.PromptedResources = resources; var obj = new MetadataPromptObject { Resources = objects }; UserInput.Request(obj, false); if (obj.Response == MetadataPromptObject.PromptResponse.Abort) planRun.MainThread.Abort(); } catch (Exception e) { Log.Debug(e); planRun.MainThread.Abort("Error occured while executing platform requests. Metadata prompt can be disabled from the Engine settings menu."); } finally { planRun.PromptWaitHandle.Set(); } }, name: "Request Metadata"); } else { planRun.PromptWaitHandle.Set(); } } /// /// Blocking Execute TestPlan. Uses ResultListeners from ResultSettings.Current. /// /// Result of test plan run as a TestPlanRun object. public TestPlanRun Execute() { return Execute(ResultSettings.Current, null); } /// Executes the test plan asynchronously /// A task returning the test plan run. public Task ExecuteAsync() { return ExecuteAsync(ResultSettings.Current, null, null, TapThread.Current.AbortToken); } /// /// Executes the test plan asynchronously. /// /// This abort token can be used to abort the operation. /// A task returning the test plan run. public Task ExecuteAsync(CancellationToken abortToken) { return ExecuteAsync(ResultSettings.Current, null, null, abortToken); } readonly TestPlanRunSummaryListener summaryListener = new TestPlanRunSummaryListener(); /// /// Execute the TestPlan as specified. /// /// ResultListeners for result outputs. /// Optional metadata parameters. /// Sub-section of test plan to be executed. Note this might include child steps of disabled parent steps. /// Cancellation token to abort the testplan /// TestPlanRun results, no StepResults. public Task ExecuteAsync(IEnumerable resultListeners, IEnumerable metaDataParameters, HashSet stepsOverride, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); TapThread.Start(() => { try { cancellationToken.Register(TapThread.Current.Abort); var testPlanRun = Execute(resultListeners, metaDataParameters, stepsOverride); tcs.SetResult(testPlanRun); } catch (Exception e) { tcs.SetException(e); } }, "Plan Thread"); return tcs.Task; } internal static ThreadHierarchyLocal executingPlanRun = new ThreadHierarchyLocal(); /// /// Execute the TestPlan as specified. Blocking. /// /// ResultListeners for result outputs. /// Optional metadata parameters. /// Sub-section of test plan to be executed. Note this might include child steps of disabled parent steps. /// /// TestPlanRun results, no StepResults. public TestPlanRun Execute(IEnumerable resultListeners, IEnumerable metaDataParameters = null, HashSet stepsOverride = null, VariableContainer variableContainer = null) { return executeInContext(resultListeners, metaDataParameters, stepsOverride, variableContainer, MAIN_SEQ_NAME); } public TestPlanRun Execute(string sequenceName, IEnumerable resultListeners, IEnumerable metaDataParameters = null, HashSet stepsOverride = null, VariableContainer variableContainer = null) { return executeInContext(resultListeners, metaDataParameters, stepsOverride, variableContainer, sequenceName); } TestPlanRun executeInContext(IEnumerable resultListeners, IEnumerable metaDataParameters, HashSet stepsOverride, VariableContainer variableContainer, string sequenceName) { using (TapThread.UsingThreadContext()) { return ExecuteByName(resultListeners, metaDataParameters, stepsOverride, sequenceName, variableContainer); } } /// /// °´Ãû³ÆÖ´ÐвâÊԼƻ® /// /// /// /// /// /// /// /// private TestPlanRun ExecuteByName(IEnumerable resultListeners, IEnumerable metaDataParameters, HashSet stepsOverride, string sequenceName, VariableContainer variableContainer = null) { if (resultListeners == null) throw new ArgumentNullException(nameof(resultListeners)); ResultParameters.ParameterCache.LoadCache(); if (PrintTestPlanRunSummary && !resultListeners.Contains(summaryListener)) resultListeners = resultListeners.Concat(new IResultListener[] { summaryListener }); resultListeners = resultListeners.Where(r => r is IEnabledResource ? ((IEnabledResource)r).IsEnabled : true); IList steps; TestStepList list; if (stepsOverride == null) { list = GetStepsByName(sequenceName); steps = list; } else { var tempStep = GetStepsByName(sequenceName); if (tempStep == null) { throw new ArgumentException("No Name"); } list = tempStep; // Remove steps that are already included via their parent steps. foreach (var step in stepsOverride) { if (step == null) throw new ArgumentException("stepsOverride may not contain null", nameof(stepsOverride)); var p = step.GetParent(); while (p != null) { if (stepsOverride.Contains(p)) throw new ArgumentException("stepsOverride may not contain steps and their parents.", nameof(stepsOverride)); p = p.GetParent(); } } steps = Utils.FlattenHeirarchy(tempStep, step => step.ChildTestSteps).Where(stepsOverride.Contains).ToList(); } long initTimeStamp = Stopwatch.GetTimestamp(); var initTime = DateTime.Now; Log.Info("-----------------------------------------------------------------"); var logStream = new HybridStream(); var planRunLog = new FileTraceListener(logStream) { IsRelative = true }; OpenTap.Log.AddListener(planRunLog); var allSteps = Utils.FlattenHeirarchy(steps, step => step.ChildTestSteps); var allEnabledSteps = Utils.FlattenHeirarchy(steps.Where(x => x.Enabled), step => step.GetEnabledChildSteps()); var enabledSinks = new HashSet(); TestStepExtensions.GetObjectSettings(allEnabledSteps, true, null, enabledSinks); if (enabledSinks.Count > 0) { var sinkListener = new ResultSinkListener(enabledSinks); resultListeners = resultListeners.Append(sinkListener); } Log.Info("Starting TestPlan '{0}' on {1}, {2} of {3} TestSteps enabled.", Name, initTime, allEnabledSteps.Count, allSteps.Count); // Reset step verdict. foreach (var step in allSteps) { if (step.Verdict != Verdict.NotSet) { step.Verdict = Verdict.NotSet; step.OnPropertyChanged("Verdict"); } } if (currentExecutionState != null) { currentExecutionState.ResultListenersSealed = false; // load result listeners that are _not_ used in the previous runs. // otherwise they wont get opened later. foreach (var rl in resultListeners) { currentExecutionState.AddResultListener(rl); } } TestPlanRun execStage; bool continuedExecutionState = false; if (currentExecutionState != null) { execStage = new TestPlanRun(currentExecutionState, initTime, initTimeStamp, variableContainer); continuedExecutionState = true; } else { execStage = new TestPlanRun(this, resultListeners.ToList(), initTime, initTimeStamp, variableContainer); if (stepsOverride != null) { var overrides = stepsOverride.Select(o => o.Id.ToString()).ToArray(); // The order of the guids does not really matter. // Only that the order is the same across runs Array.Sort(overrides); execStage.Parameters.Add(new ResultParameter(TestPlanRun.SpecialParameterNames.StepOverrideList, string.Join(",", overrides))); } execStage.Start(); execStage.Parameters.AddRange(PluginManager.GetPluginVersions(allEnabledSteps)); execStage.ResourceManager.ResourceOpened += r => { execStage.Parameters.AddRange(PluginManager.GetPluginVersions(new List { r })); }; } execStage.LocalsRuntime = VariableContext.FromTestVariables(list.Variables); if (metaDataParameters != null) execStage.Parameters.AddRange(metaDataParameters); var prevExecutingPlanRun = executingPlanRun.LocalValue; executingPlanRun.LocalValue = execStage; CurrentRun = execStage; failState runWentOk = failState.StartFail; // ûɶÓúÃÏñ // ³õʼ»¯ÔËÐÐʱ±äÁ¿³Ø //FileGlobalContext.Add(execStage, execStage.runtimeVariablePool); // ReSharper disable once InconsistentNaming var preRun_Run_PostRunTimer = Stopwatch.StartNew(); try { execStage.FailedToStart = true; // Set it here in case OpenInternal throws an exception. Could happen if a step is missing an instrument OpenInternal(execStage, continuedExecutionState, allEnabledSteps, Array.Empty()); execStage.WaitForSerialization(); execStage.ResourceManager.BeginStep(execStage, this, TestPlanExecutionStage.Execute, TapThread.Current.AbortToken); if (continuedExecutionState) { // Since resources are not opened, getting metadata cannot be done in the wait for resources continuation // like shown in TestPlanRun. Instead we do it here. foreach (var res in execStage.ResourceManager.Resources) execStage.Parameters.AddRange(ResultParameters.GetMetadataFromObject(res)); } runWentOk = failState.ExecFail; //important if test plan is aborted and runWentOk is never returned. runWentOk = execTestPlan(execStage, steps); } catch (Exception e) { if (e is OperationCanceledException && execStage.MainThread.AbortToken.IsCancellationRequested) { Log.Warning(String.Format("TestPlan aborted. ({0})", e.Message)); execStage.UpgradeVerdict(Verdict.Aborted); } else if (e is ThreadAbortException) { // It seems this actually never happens. Log.Warning("TestPlan aborted."); execStage.UpgradeVerdict(Verdict.Aborted); //Avoid entering the finally clause. Thread.Sleep(500); } else if (e is System.ComponentModel.LicenseException) { execStage.Exception = e; Log.Error(e.Message); execStage.UpgradeVerdict(Verdict.Error); } else { execStage.Exception = e; Log.Warning("TestPlan aborted."); Log.Error(e.Message); Log.Debug(e); execStage.UpgradeVerdict(Verdict.Error); } } finally { execStage.FailedToStart = (runWentOk == failState.StartFail); try { finishTestPlanRun(execStage, preRun_Run_PostRunTimer, runWentOk, planRunLog, logStream); } catch (Exception ex) { Log.Error("Error while finishing TestPlan."); Log.Debug(ex); } OpenTap.Log.RemoveListener(planRunLog); planRunLog.Dispose(); logStream.Dispose(); // Clean all test steps StepRun, otherwise the next test plan execution will be stuck at TestStep.DoRun at steps that does not have a cleared StepRun. foreach (var step in allSteps) step.StepRun = null; executingPlanRun.LocalValue = prevExecutingPlanRun; CurrentRun = prevExecutingPlanRun; } return execStage; } private TestPlanRun DoExecute(IEnumerable resultListeners, IEnumerable metaDataParameters, HashSet stepsOverride, VariableContainer variableContainer = null) { return ExecuteByName(resultListeners, metaDataParameters, stepsOverride, MAIN_SEQ_NAME, variableContainer); } /// /// Execute the TestPlan as specified. Blocking. /// /// ResultListeners for result outputs. /// Metadata parameters. /// TestPlanRun results, no StepResults. public TestPlanRun Execute(IEnumerable resultListeners, IEnumerable metaDataParameters) { return Execute(resultListeners, metaDataParameters, null); } TestPlanRun currentExecutionState = null; /// true if the plan is in its open state. [AnnotationIgnore] public bool IsOpen { get { return currentExecutionState != null; } } /// /// Opens all resources referenced in this TestPlan (Instruments/DUTs/ResultListeners). /// This can be called before to manually control the opening/closing of the resources. /// public void Open() { Open(ResultSettings.Current); } /// /// Opens all resources referenced in this TestPlan (Instruments/DUTs/ResultListeners). /// This can be called before to manually control the opening/closing of the resources. /// /// Result listeners used in connection with the test plan. public void Open(IEnumerable listeners) { Open(listeners, Array.Empty()); } /// /// Opens all resources referenced in this TestPlan (Instruments/DUTs/ResultListeners). /// This can be called before to manually control the opening/closing of the resources. /// It can also be called multiple times to add more resources. /// /// Additional resources added as 'static' resources. public void Open(IEnumerable additionalResources) { Open(ResultSettings.Current, additionalResources); } /// /// Opens all resources referenced in this TestPlan (Instruments/DUTs/ResultListeners). /// This can be called before to manually control the opening/closing of the resources. /// It can also be called multiple times to add more resources. /// /// Result listeners used in connection with the test plan. /// Additional resources added as 'static' resources. public void Open(IEnumerable listeners, IEnumerable additionalResources) { if (listeners == null) throw new ArgumentNullException(nameof(listeners)); if (PrintTestPlanRunSummary) listeners = listeners.Concat(new IResultListener[] { summaryListener }); if (IsRunning) throw new InvalidOperationException("This TestPlan is already running."); try { var allSteps = Utils.FlattenHeirarchy(Steps.Where(x => x.Enabled), step => step.GetEnabledChildSteps()).ToList(); Stopwatch timer = Stopwatch.StartNew(); if (currentExecutionState == null) { currentExecutionState = new TestPlanRun(this, listeners.ToList(), DateTime.Now, Stopwatch.GetTimestamp(), null, true); currentExecutionState.Start(); } OpenInternal(currentExecutionState, false, allSteps, additionalResources); try { currentExecutionState.ResourceManager.WaitUntilAllResourcesOpened(TapThread.Current.AbortToken); } catch (Exception ex) { Log.Warning("Caught error while opening resources! See error message for details."); ex.RethrowInner(); } Log.Debug(timer, "TestPlan opened."); } catch { // If there is an error, reset the state to allow calling open again later // when the user has fixed the error. if (currentExecutionState != null) currentExecutionState.ResourceManager.EndStep(this, TestPlanExecutionStage.Open); if (monitors != null) foreach (var item in monitors) item.ExitTestPlanRun(currentExecutionState); currentExecutionState = null; throw; } } private void OpenInternal(TestPlanRun run, bool isOpen, List steps, IEnumerable additionalResources) { monitors = TestPlanRunMonitors.GetCurrent(); // Enter monitors foreach (var item in monitors) item.EnterTestPlanRun(run); run.ResourceManager.EnabledSteps = steps; run.ResourceManager.StaticResources = run.ResultListeners.Concat(additionalResources).ToArray(); run.ResultListenersSealed = true; if (!isOpen) run.ResourceManager.BeginStep(run, this, TestPlanExecutionStage.Open, TapThread.Current.AbortToken); } /// /// Closes all resources referenced in this TestPlan (Instruments/DUTs/ResultListeners). /// This should be called if was called earlier to manually close the resources again. /// public void Close() { if (IsRunning) throw new InvalidOperationException("Cannot close TestPlan while it is running."); if (currentExecutionState == null) throw new InvalidOperationException("Call open first."); Stopwatch timer = Stopwatch.StartNew(); currentExecutionState.ResourceManager.EndStep(this, TestPlanExecutionStage.Open); // If we locked the setup earlier, unlock it now that all recourses has been closed: foreach (var item in monitors) item.ExitTestPlanRun(currentExecutionState); currentExecutionState = null; Log.Debug(timer, "TestPlan closed."); } } // This object has a special data annotator that embeds metadata properties // from Resources into it. class MetadataPromptObject { public string Name { get; private set; } = "Please enter test plan metadata."; [Browsable(false)] public IEnumerable Resources { get; set; } public enum PromptResponse { OK, Abort } [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)] [Submit] public PromptResponse Response { get; set; } } }