using System.Reflection;
using System.Runtime.CompilerServices;
using Bpmn.Interchange;
using Bpmn.Model;
using Bpmn.Semantics;
using Elsa.Bpmn.Features;
using Elsa.Bpmn.Interchange.Features;
namespace Elsa.Bpmn.Interchange.UnitTests;
///
/// Guards D12 (see #7909/#7934): no type under the Elsa.Bpmn* assemblies may reimplement BPMN
/// semantics that the Bpmn.* library already provides. This has already happened once, in
/// elsa-foundation, where a parallel BpmnElement/BpmnGraph/etc. semantics core grew alongside the
/// shared library because only the interchange half was migrated. A review habit did not catch
/// that; this test exists to.
/// This guard lives in the interchange test project, not the Elsa.Bpmn one, because it is the only
/// project that sees both Elsa.Bpmn and Elsa.Bpmn.Interchange transitively without inverting the
/// module layering that the guard itself protects.
/// The dependency direction (that Elsa.Bpmn/Elsa.Bpmn.Interchange still reference the Bpmn.*
/// packages rather than dropping them and keeping a local copy) is not asserted at runtime here;
/// it is enforced at compile time by the typeof bindings below, see the comment there.
/// This guard also proves its own detector: a fixture type below deliberately collides with a
/// library type name so the suite shows the check can fail, not just pass.
///
public class BpmnLibraryDuplicationGuardTests
{
// Every type name the assembly defines is checked, not just public ones: an internal
// reimplementation of BpmnGraph is the same disease as a public one.
// These typeof bindings are load-bearing beyond just locating the assemblies below: dropping
// the Bpmn.Semantics package reference from Elsa.Bpmn, or the Bpmn.Interchange package
// reference from Elsa.Bpmn.Interchange, fails this project's build outright (verified:
// CS0234), because typeof(BpmnInterpreter) and typeof(BpmnXmlReader) respectively have
// nowhere else to resolve from. typeof(BpmnDefinitions) pins Bpmn.Model the same way, though
// Bpmn.Model also arrives transitively via Bpmn.Semantics and Bpmn.Interchange, so dropping
// only its own package reference from Elsa.Bpmn does not by itself break the build; the
// binding is kept for symmetry and because BpmnDefinitions itself must still be reachable for
// the guard's collision check to run. This compile-time enforcement is stronger than a
// runtime assertion could be where it applies, so no test asserts the dependency direction.
// Do not remove these bindings or replace them with string-based assembly lookup.
private static readonly Assembly BpmnModelAssembly = typeof(BpmnDefinitions).Assembly;
private static readonly Assembly BpmnSemanticsAssembly = typeof(BpmnInterpreter).Assembly;
private static readonly Assembly BpmnInterchangeAssembly = typeof(BpmnXmlReader).Assembly;
private static readonly Assembly ElsaBpmnAssembly = typeof(BpmnFeature).Assembly;
private static readonly Assembly ElsaBpmnInterchangeAssembly = typeof(BpmnInterchangeFeature).Assembly;
[Fact]
public void ElsaBpmnAssembly_DoesNotDuplicateBpmnModelOrBpmnSemanticsTypeNames()
{
AssertNoTypeNameCollisions(ElsaBpmnAssembly, BpmnModelAssembly, BpmnSemanticsAssembly);
}
[Fact]
public void ElsaBpmnInterchangeAssembly_DoesNotDuplicateBpmnLibraryTypeNames()
{
// Bpmn.Interchange owns BpmnXmlReader/BpmnXmlWriter and the import analyzer per the
// library/module split, so its type names are forbidden here too, alongside Bpmn.Model
// and Bpmn.Semantics (which Elsa.Bpmn.Interchange can reach transitively).
AssertNoTypeNameCollisions(ElsaBpmnInterchangeAssembly, BpmnModelAssembly, BpmnSemanticsAssembly, BpmnInterchangeAssembly);
}
[Fact]
public void TypeNameCollisionDetection_ActuallyDetectsACollision()
{
var collisions = FindTypeNameCollisions(typeof(BpmnLibraryDuplicationGuardTests).Assembly, BpmnSemanticsAssembly);
Assert.NotEmpty(collisions);
Assert.Contains(collisions, message => message.Contains(nameof(BpmnGraph)));
}
private static void AssertNoTypeNameCollisions(Assembly elsaAssembly, params Assembly[] libraryAssemblies)
{
var collisions = FindTypeNameCollisions(elsaAssembly, libraryAssemblies);
Assert.True(
collisions.Count == 0,
$"{elsaAssembly.GetName().Name} must not reimplement BPMN semantics the library already provides:{Environment.NewLine}{string.Join(Environment.NewLine, collisions)}");
}
private static IReadOnlyList FindTypeNameCollisions(Assembly elsaAssembly, params Assembly[] libraryAssemblies)
{
var libraryTypesByName = libraryAssemblies
.SelectMany(a => a.GetTypes())
.Where(t => !IsCompilerGenerated(t))
.GroupBy(t => t.Name)
.ToDictionary(g => g.Key, g => g.First());
return elsaAssembly
.GetTypes()
.Where(t => !IsCompilerGenerated(t))
.Where(t => libraryTypesByName.ContainsKey(t.Name))
.Select(t => $"{t.FullName} (in {elsaAssembly.GetName().Name}) duplicates {libraryTypesByName[t.Name].FullName} (in {libraryTypesByName[t.Name].Assembly.GetName().Name})")
.OrderBy(message => message)
.ToList();
}
// The declaring-type walk is load-bearing, not defensive. A collection expression targeting an
// IReadOnlyList makes the compiler synthesize a <>z__ReadOnlySingleElementList into the
// assembly; that outer type is filtered by its name, but its nested Enumerator struct is named
// plainly and carries no CompilerGeneratedAttribute of its own, so it reads as a hand-written type
// colliding with the identically synthesized one in Bpmn.Model. That is a false positive: nothing a
// person wrote is ever nested inside a <>-named type, and this guard's own fixture (BpmnGraph,
// nested in an ordinary class) still trips the detector. Without this, the guard fails the moment
// anyone writes a single-element collection expression in Elsa.Bpmn*, which teaches people to work
// around it rather than to trust it.
private static bool IsCompilerGenerated(Type type) =>
type.Name.StartsWith('<') ||
type.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false) ||
(type.DeclaringType is { } declaringType && IsCompilerGenerated(declaringType));
///
/// Exists solely as this guard's own fixture: its name deliberately collides with
/// Bpmn.Semantics' BpmnGraph so
/// can prove the detector fires. Must not be renamed.
///
private sealed class BpmnGraph;
}