// 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 System; using System.Collections.Generic; using System.Linq; using System.Xml.Serialization; using System.IO; using System.Threading; using System.Diagnostics; using System.ComponentModel; using System.Collections.ObjectModel; using System.Text.RegularExpressions; using OpenTap.Addin; namespace OpenTap { /// /// Class containing a test plan. /// [AllowAnyChild] public partial class TestPlan : INotifyPropertyChanged, ITestStepParent { public static readonly string MAIN_SEQ_NAME = "MainSequence"; internal static readonly TraceSource Log = OpenTap.Log.CreateSource(nameof(TestPlan)); ITestPlanRunMonitor[] monitors; // For locking/unlocking or generally monitoring test plan start/stop. /// Field for external test plan parameters. [XmlIgnore] [AnnotationIgnore] public ExternalParameters ExternalParameters { get; } /// /// Gets or sets a list of child TestSteps. /// [Browsable(false)] [AnnotationIgnore] [XmlIgnore] public TestStepList Steps { get => GetStepsByName(MAIN_SEQ_NAME); set { if (value == Sequences[0]) return; Sequences[0] = value; value.SequenceName = MAIN_SEQ_NAME; Sequences[0].Parent = this; OnPropertyChanged(nameof(Steps)); OnPropertyChanged(nameof(ChildTestSteps)); } } //[Browsable(false)] //[AnnotationIgnore] //public TestStepList Steps //{ // get => _Steps; // set // { // if (value == _Steps) return; // _Steps = value; // value.SequenceName = MAIN_SEQ_NAME; // _Steps.Parent = this; // OnPropertyChanged(nameof(Steps)); // OnPropertyChanged(nameof(ChildTestSteps)); // } //} /// [AnnotationIgnore] [XmlIgnore] public TestStepList ChildTestSteps => GetStepsByName(MAIN_SEQ_NAME);// _Steps; private ObservableCollection sequences; public ObservableCollection Sequences { get => sequences; set { if (value == null) { sequences = new ObservableCollection(); } else { sequences = value; foreach (var seq in value) { seq.Parent = this; Utils.FlattenHeirarchy(seq, x => x.ChildTestSteps).ForEach(s => s.Root = this); } } OnPropertyChanged(nameof(Sequences)); OnPropertyChanged(nameof(Steps)); OnPropertyChanged(nameof(ChildTestSteps)); } } public TestStepList GetStepsByName(string name) { return Sequences.FirstOrDefault(s => s.SequenceName == name); } // ObservableCollection Sequences; /// /// Always null for test plan. /// [XmlIgnore] [Browsable(false)] [AnnotationIgnore] public ITestStepParent Parent { get => null; set { } } /// Gets the subset of steps that are enabled. [XmlIgnore] [AnnotationIgnore] public ReadOnlyCollection EnabledSteps => Steps .GetSteps(TestStepSearch.EnabledOnly) .ToList() .AsReadOnly(); /// /// Gets or sets the name. This is usually the name of the file where the test plan is saved, without the .TapPlan extension. /// [XmlIgnore] [MetaData(macroName: "TestPlanName")] [Browsable(true)] [Display("Name", "The test plan name. This is usually based on the name of the file to which it is saved.", Order: 1)] public string Name { get { if (Path == null) return "Untitled"; return System.IO.Path.GetFileNameWithoutExtension(Path); } } /// /// A synchronous event that allows breaking the execution of the TestPlan by blocking the TestPlan execution thread. /// It is raised prior to executing the method of each in the TestPlan. /// TestSteps may also raise this event from inside the method. /// public event EventHandler BreakOffered; /// True if the test plan is waiting in a break. [AnnotationIgnore] public bool IsInBreak => breakRefCount > 0; int breakRefCount = 0; /// Raises the event. /// If called by Tap internal code at the start of a run, the TestStepRun.Verdict will be equal to pending. /// If called via a user written TestStep, the TestStepRun.Verdict will be equal to running. /// Event information to send to registered event handlers. internal void OnBreakOffered(BreakOfferedEventArgs args) { if (args == null) return; if (args.TestStepRun == null) return; if (BreakOffered != null) { Interlocked.Increment(ref breakRefCount); try { BreakOffered(this, args); } finally { Interlocked.Decrement(ref breakRefCount); } } //Just in case the user requested to abort TapThread.ThrowIfAborted(); } bool locked = false; /// /// Locks the TestPlan to signal that it should not be changed. /// The GUI respects this. /// [XmlAttribute] [Display("Locked", "Checking this makes the test plan read-only.", Order: 2)] [DefaultValue(false)] public bool Locked { get => locked; set { if (value != locked) { locked = value; OnPropertyChanged(nameof(Locked)); } } } TestPlanRun _currentRun; TestPlanRun CurrentRun { set { _currentRun = value; OnPropertyChanged(nameof(IsRunning)); } get => _currentRun; } /// True if this TestPlan is currently running. [AnnotationIgnore] public bool IsRunning => CurrentRun != null; /// /// Can be set to true to allow editing the test plan while it's paused. /// /// This is set to non-editorbrowsable to avoid it being modified from a test step. [AnnotationIgnore] [EditorBrowsable(EditorBrowsableState.Never)] [XmlIgnore] public bool AllowEditWhilePaused { get; set; } /// /// Gets if the test plan is currently editable. This will never be true if the test plan is running(not paused) or locked. /// public bool AllowEdit => !Locked && (!IsRunning || (IsRunning && IsInBreak && AllowEditWhilePaused)); /// public TestPlan() { Sequences = new ObservableCollection { new TestStepList() { SequenceName = MAIN_SEQ_NAME } }; //_Steps = new TestStepList(); //_Steps.Parent = this; ExternalParameters = new ExternalParameters(this); } /// Gets or sets if the test plan XML for this test plan should be cached. [XmlIgnore] [AnnotationIgnore] public bool CacheXml { get; set; } byte[] xmlCache = null; /// Flushes the test plan XML cache. public void FlushXmlCache() => xmlCache = null; /// Get the cached XML (or null if it is not cached) public byte[] GetCachedXml() => xmlCache; #region Load/Save TestPlan /// /// Saves this TestPlan to a stream with a specific serializer. /// /// The in which to save the TestPlan. /// Optional. The serializer use for deserialization. If set to null, one will be created. public void Save(Stream stream, TapSerializer serializer) { if (xmlCache != null) { stream.Write(xmlCache, 0, xmlCache.Length); return; } if (stream == null) throw new ArgumentNullException(nameof(stream)); serializer = serializer ?? new TapSerializer(); if (CacheXml) { var str = new MemoryStream(); serializer.Serialize(str, this); xmlCache = str.ToArray(); str.CopyTo(stream); } else { serializer.Serialize(stream, this); } } /// /// Saves this TestPlan to a stream; /// /// The in which to save the TestPlan. public void Save(Stream stream) => Save(stream, null); /// /// Saves this TestPlan to a file path. /// /// The file path in which to save the TestPlan. public void Save(string filePath) { if (filePath == null) throw new ArgumentNullException(nameof(filePath)); using (StreamWriter file = new StreamWriter(filePath)) { Save(file.BaseStream); } Path = filePath; OnPropertyChanged(nameof(Path)); OnPropertyChanged(nameof(Name)); } /// Load a TestPlan. /// The file path of the TestPlan. /// Returns the new test plan. public static TestPlan Load(string filePath) => Load(filePath, false); /// Load a TestPlan. /// The file path of the TestPlan. /// Gets or sets if the XML should be cached. /// Returns the new test plan. public static TestPlan Load(string filePath, bool cacheXml) { if (filePath == null) throw new ArgumentNullException(nameof(filePath)); var timer = Stopwatch.StartNew(); // Open document using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { var loadedPlan = Load(fs, filePath, cacheXml); Log.Info(timer, "Loaded test plan from {0}", filePath); return loadedPlan; } } enum PlanLoadErrorResponse { Ignore, Abort } /// /// Exception occuring when a test plan loads. /// public class PlanLoadException : Exception { /// /// public PlanLoadException(string message) : base(message) { } } class PlanLoadError { [Browsable(true)] [Layout(LayoutMode.FullRow, 2)] public string Message { get; private set; } = "Parts of the test plan did not load correctly. See the log for more details.\n\nDo you want to ignore these errors and use a corrupt test plan?"; public string Name { get; private set; } = "Errors occured while loading test plan."; [Layout(LayoutMode.FullRow | LayoutMode.FloatBottom)] [Submit] // this should be Abort by default, so that --non-interactive fails to start. public PlanLoadErrorResponse Response { get; set; } = PlanLoadErrorResponse.Abort; } /// Load a TestPlan. /// The stream from which the file is actually loaded. /// The path to the file. This will be tha value of on the new TestPlan. /// Returns the new test plan. public static TestPlan Load(Stream stream, string path) => Load(stream, path, false); /// Load a TestPlan. /// The stream from which the file is actually loaded. /// The path to the file. This will be tha value of on the new TestPlan. /// Gets or sets if the XML should be cached. /// Optionally the serializer used for deserializing the test plan. /// Returns the new test plan. public static TestPlan Load(Stream stream, string path, bool cacheXml, TapSerializer serializer = null) { return Load(stream, path, cacheXml, serializer, false); } internal static TestPlan Load(Stream stream, string path, bool cacheXml, TapSerializer serializer, bool IgnoreLoadErrors) { if (stream == null) throw new ArgumentNullException(nameof(stream)); if (cacheXml) { var memstr = new MemoryStream(); stream.CopyTo(memstr); stream = memstr; memstr.Seek(0, SeekOrigin.Begin); } serializer = serializer ?? new TapSerializer(); var plan = (TestPlan)serializer.Deserialize(stream, type: TypeData.FromType(typeof(TestPlan)), path: path); var errors = serializer.Errors; if (IgnoreLoadErrors == false && errors.Any()) { // eventual errors were already printed. var err = new PlanLoadError(); UserInput.Request(err, false); var respValue = err.Response; if (respValue == PlanLoadErrorResponse.Abort) { throw new PlanLoadException(string.Join("\n", errors)); } } if (cacheXml) { plan.CacheXml = true; plan.xmlCache = ((MemoryStream)stream).ToArray(); } return plan; } /// /// Reload a TestPlan transferring the current execution state of the current plan to the new one. /// /// The filestream from which the plan loaded /// Returns the new test plan. public TestPlan Reload(Stream filestream) { if (filestream == null) throw new ArgumentNullException("filestream"); if (this.IsRunning) throw new InvalidOperationException("Cannot reload while running."); var serializer = new TapSerializer(); var plan = (TestPlan)serializer.Deserialize(filestream, type: TypeData.FromType(typeof(TestPlan)), path: Path); plan.currentExecutionState = this.currentExecutionState; plan.Path = this.Path; return plan; } /// /// Returns the list of plugins that are required to use the test plan. /// /// /// public static List GetPluginsRequiredToLoad(string filepath) { if (filepath == null) throw new ArgumentNullException("filepath"); List types = new List(); string[] lines = File.ReadAllLines(filepath); Regex rx = new Regex("[^\"]+)\""); foreach (string line in lines) { Match match = rx.Match(line); if (match.Success) { types.Add(match.Groups["name"].Value); } } return types; } #endregion #region INotifyPropertyChanged Members /// /// Event handler to on property changed. /// public event PropertyChangedEventHandler PropertyChanged; /// /// When a property changes this function is called. /// /// Inputs the string for the property changed. protected virtual void OnPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); UserInput.NotifyChanged(this, name); } } #endregion private string path; /// /// Gets where this plan was last saved or loaded from. It might be null. /// [XmlIgnore] [AnnotationIgnore] public string Path { get => path; internal set { path = value; VisualPath = value; } } /// /// ÐéÄâ·¾¶ /// [XmlIgnore] [AnnotationIgnore] public string VisualPath { get; set; } /// The directory where the test plan is stored. [MetaData(macroName: "TestPlanDir")] [AnnotationIgnore] public string Directory { get { try { return string.IsNullOrWhiteSpace(Path) ? null : System.IO.Path.GetDirectoryName(Path); } catch { // probably an invalid path. return null; } } } readonly HashSet<(IMemberData, object)> registeredParameters = new HashSet<(IMemberData, object)>(); internal void RegisterParameter(IMemberData member, object source) { lock (registeredParameters) registeredParameters.Add((member, source)); } internal void UnregisterParameter(IMemberData member, object source) { lock (registeredParameters) registeredParameters.Remove((member, source)); } internal bool IsRegistered(IMemberData member, object source) { lock (registeredParameters) return registeredParameters.Contains((member, source)); } public void InsertStep(string name, int index, ITestStep testStep) { testStep.Root = this; GetStepsByName(name).Insert(index, testStep); } public void AddStep(string name, ITestStep testStep) { testStep.Root = this; GetStepsByName(name).Add(testStep); } } /// /// Provides data for the event. /// public class BreakOfferedEventArgs : EventArgs { /// /// Details of the currently running (or about to be running) step. /// public TestStepRun TestStepRun { get; private set; } /// /// Indicates whether the this event was raised by the engine when it starts running a TestStep (true) /// or during the run of a TestStep from within a TestStep itself (false). /// /// This value can also be determined as TestStepRun.Verdict == TestStep.VerdictType.Pending. public bool IsTestStepStarting { get; private set; } /// /// Specifies that the current step should not be run, but instead flow control should move to another step. /// It is up to the test step to honor this, in some cases it will not be possible. When supported, /// TestStepRun.SupportsJumpTo should be set to true. /// public Guid? JumpToStep { get; set; } internal BreakOfferedEventArgs(TestStepRun testStepRun, bool isTestStepStarting) { IsTestStepStarting = isTestStepStarting; TestStepRun = testStepRun; } } }