// 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.Diagnostics; using System.Linq; namespace OpenTap { /// /// This class represents a resource in a dependency tree, and contains lists of the resources it depends on. /// [DebuggerDisplay("{Depender} : {Resource}")] internal class ResourceNode : IResourceReferences { /// /// The resource this node represents. /// public IResource Resource { get; set; } /// The property that references this resource. public IMemberData Depender { get; } /// /// The resources that this node depends on. These are marked with . /// internal readonly List WeakDependencies; /// /// The resources that this node depends on. These will be opened before this nodes resource. /// internal readonly List StrongDependencies; /// /// TestSteps (or other IResources) that uses this resource. These are the reason this resources needs to be opened when running a TestPlan. /// public List References { get; } = new List(); internal ResourceNode(IResource resource) { this.Resource = resource; this.WeakDependencies = new List(); this.StrongDependencies = new List(); } internal ResourceNode(IResource resource, IMemberData prop, IEnumerable weakDeps, IEnumerable strongDeps) { this.Resource = resource; this.Depender = prop; this.WeakDependencies = weakDeps.ToList(); this.StrongDependencies = strongDeps.ToList(); } } internal class ResourceDependencyAnalyzer { private static TraceSource Log = OpenTap.Log.CreateSource("Dependency Analyzer"); private struct ResourceDep { public override bool Equals(object obj) { if (obj is ResourceDep other) return other.Behavior == Behavior && other.Resource == Resource && other.Depender == Depender; return false; } public override int GetHashCode() => Behavior.GetHashCode() * 3120732 + (Resource?.GetHashCode() ?? 73289132) * 134017414 + (Depender?.GetHashCode() ?? 321312) * 730921632; public readonly ResourceOpenBehavior Behavior; public readonly IResource Resource; public readonly IMemberData Depender; // this is important in case the resource is null public ResourceDep(ResourceOpenBehavior behavior, IResource resource, IMemberData dep) { this.Behavior = behavior; this.Resource = resource; this.Depender = dep; } public ResourceDep(IResource resource) { Depender = null; this.Resource = resource; Behavior = ResourceOpenBehavior.Before; } } private ResourceDep FilterProps(IResource o, IMemberData pi) { var behavior = ResourceOpenBehavior.Before; var attr = pi.GetAttribute(); if (attr != null) behavior = attr.Behavior; return new ResourceDep(behavior, o, pi); } ResourceDep[] getManyResources(T[] steps) { if (steps.Length == 0) return Array.Empty(); var result = new HashSet(); TestStepExtensions.GetObjectSettings(steps, true, FilterProps, result); result.RemoveWhere(dep => dep.Behavior == ResourceOpenBehavior.Ignore); return result.ToArray(); } private IEnumerable GetResources(T Step) { if (Step == null) return Array.Empty(); return getManyResources(new[] { Step }); } private ResourceNode Analyze(ResourceDep resource) { var resources = GetResources(resource.Resource); return new ResourceNode(resource.Resource,resource.Depender, weakDeps: resources.Where(x => x.Behavior == ResourceOpenBehavior.InParallel).Select(x => x.Resource), strongDeps: resources.Where(x => x.Behavior == ResourceOpenBehavior.Before).Select(x => x.Resource) ); } private List GetResourceTree(ICollection resources) { Queue allResources = new Queue(resources); HashSet knownNodes = new HashSet(); List allNodes = new List(); ResourceDep x; while (allResources.Count > 0) { x = allResources.Dequeue(); if (x.Resource == null) { if (knownNodes.Any(k => k.Resource == x.Resource && k.Depender == x.Depender)) continue; } else { if (knownNodes.Any(k => k.Resource == x.Resource)) continue; } knownNodes.Add(x); var newNode = Analyze(x); allNodes.Add(newNode); if (x.Resource != null) { List newNodes = newNode.WeakDependencies.Concat(newNode.StrongDependencies).ToList(); foreach (IResource node in newNodes.Except(knownNodes.Select(k => k.Resource))) allResources.Enqueue(new ResourceDep(node)); } } return allNodes; } private void ExpandTree(List nodes) { var lut = nodes.Where(x => x.Resource != null).ToDictionary(x => x.Resource, x => x); bool changed; do { changed = false; foreach (var n in nodes) { if (n == null) continue; var newDeps = n.StrongDependencies.Where(x => x != null) .SelectMany(x => lut[x].StrongDependencies.Concat(lut[x].WeakDependencies)) .Except(n.StrongDependencies) .ToArray(); if (newDeps.Length > 0) { changed = true; n.StrongDependencies.AddRange(newDeps); } } } while (changed); } #region Strongly connected component algorithm // Can be used to find all strongly connected components(circular references) in the graph made up of the resources and the dependencies between them. // Implemented based on pseudo code from here: https://en.wikipedia.org/w/index.php?title=Tarjan%27s_strongly_connected_components_algorithm&oldid=774903237 private class Vertex { internal int index, lowlink; internal bool onStack; internal ResourceNode node; } private void StrongConnect(Vertex v, Dictionary V, Stack S, List> sccs, ref int index) { v.index = index; v.lowlink = index; index++; S.Push(v); v.onStack = true; // For all edges (v->w) foreach (var w in v.node.StrongDependencies.Select(n => V[n])) { if (w.index == -1) { StrongConnect(w, V, S, sccs, ref index); v.lowlink = Math.Min(v.lowlink, w.lowlink); } else if (w.onStack) { v.lowlink = Math.Min(v.lowlink, w.index); } } if (v.lowlink == v.index) { List scc = new List(); Vertex w; do { w = S.Pop(); w.onStack = false; scc.Add(w.node.Resource); } while (v != w); if (scc.Count > 1) sccs.Add(scc); } } private List> FindStronglyConnectedComponents(List tree) { List> sccs = new List>(); var V = tree.Where(n => n.Resource != null).ToDictionary(n => n.Resource, n => new Vertex { index = -1, lowlink = -1, node = n }); var S = new Stack(); int index = 0; foreach (var v in V.Values) if (v.index == -1) StrongConnect(v, V, S, sccs, ref index); return sccs; } #endregion /// /// Finds all IResource properties on a list of references. The references can be any class derived from ITapPlugin, such as ITestStep or IResource. /// If a reference supplied in the list is a IResource itself it will be added to the resulting list. /// internal List GetAllResources(object[] references, out bool errorDetected) { errorDetected = false; ResourceDep[] stepResources = getManyResources(references); var resourceDeps = new List(stepResources); foreach (var reference in references) { if(reference is IResource res) resourceDeps.Add(new ResourceDep(res)); } List tree = GetResourceTree(resourceDeps); ExpandTree(tree); // Check that no resources have direct references to itself. foreach (var scc in tree.Where(x => x.StrongDependencies.Any(dep => dep == x.Resource))) { foreach (var dep in scc.StrongDependencies) { if (dep == scc.Resource) { errorDetected = true; Log.Error("Resource is referencing itself: {0}", scc.Resource); break; } } } // Figure out if all resources has their resource properties set foreach(var leaf in tree) { if(leaf.StrongDependencies.Any(s => s is null)) { errorDetected = true; Log.Error($"Resource setting not set on resource {leaf.Resource}. Please configure or disable the resource."); } } if (errorDetected) // If any resources has resource properties which is not set, let's return early, because FindStronglyConntectedComponents method below will throw a confusing error in this case. return tree; // Figure out if there are circular references, and list the circular references in an exception each. var sccs = FindStronglyConnectedComponents(tree); if (sccs.Count > 0) { errorDetected = true; foreach (var scc in sccs) Log.Error(string.Format("Circular references between resources: {0}", string.Join(",", scc.Select(res => res.Name)))); } //Add users of resources foreach (var r in references) { TestStepExtensions.GetObjectSettings(r, true, (res, prop) => { var nodes = tree.Where(n => n.Resource == res); if (nodes.Count() > 1) { // Normally we would expect that the tree only contains one node representing each resource. // In case of null resources however, we want one node per property, such that ILockManager.BeforeOpen() // has a chance to set each property to a different resource instance. if (res != null) throw new Exception($"Duplicate entry for Resource '{res.Name}' in tree."); nodes = nodes.Where(n => n.Depender == prop); } var nodeRepresentingResource = nodes.FirstOrDefault(); if (nodeRepresentingResource != null) nodeRepresentingResource.References.Add(new ResourceReference(r, prop)); return nodeRepresentingResource; }, new HashSet()); } return tree; } } }