// 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;
}
}
}