using System.Reflection; using FinalFactory.Rendezvous.Contracts; namespace FinalFactory.Rendezvous.Tests.Contracts; public sealed class PublicApiCompatibilityTests { [Fact] public void ContractsPublicApiMatchesTheV1Snapshot() { string snapshot = CreateSnapshot(typeof(ContractLimits).Assembly); string expected = ContractTestFiles.Read("contracts-public-api.txt"); if (expected == "SNAPSHOT_PENDING" && Environment.GetEnvironmentVariable("RENDEZVOUS_UPDATE_CONTRACT_SNAPSHOT") == "1") { string snapshotPath = Path.Combine( ContractTestFiles.Directory, "contracts-public-api.txt"); File.WriteAllText(snapshotPath, snapshot + Environment.NewLine); expected = snapshot; } Assert.Equal(expected, snapshot); } private static string CreateSnapshot(Assembly assembly) { List lines = []; foreach (Type type in assembly.GetExportedTypes().OrderBy(static type => type.FullName, StringComparer.Ordinal)) { lines.Add($"TYPE {FormatType(type)}"); if (type.IsEnum) { foreach (string name in Enum.GetNames(type)) { object value = Enum.Parse(type, name); lines.Add($" ENUM {name}={Convert.ToInt64(value, System.Globalization.CultureInfo.InvariantCulture)}"); } continue; } foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) .OrderBy(static field => field.Name, StringComparer.Ordinal)) { string value = field.IsLiteral ? Convert.ToString(field.GetRawConstantValue(), System.Globalization.CultureInfo.InvariantCulture) ?? "null" : "non-literal"; lines.Add($" FIELD {FormatType(field.FieldType)} {field.Name}={value}"); } foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) .OrderBy(static constructor => FormatParameters(constructor.GetParameters()), StringComparer.Ordinal)) { lines.Add($" CTOR ({FormatParameters(constructor.GetParameters())})"); } foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) .OrderBy(static property => property.Name, StringComparer.Ordinal)) { string accessors = $"{(property.CanRead ? "get;" : string.Empty)}{(property.CanWrite ? "set;" : string.Empty)}"; lines.Add($" PROP {FormatType(property.PropertyType)} {property.Name} {{{accessors}}}"); } foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) .Where(static method => !method.IsSpecialName || method.Name.StartsWith("op_", StringComparison.Ordinal)) .OrderBy(static method => method.Name, StringComparer.Ordinal) .ThenBy(static method => FormatParameters(method.GetParameters()), StringComparer.Ordinal)) { lines.Add($" METHOD {FormatType(method.ReturnType)} {method.Name}({FormatParameters(method.GetParameters())})"); } } return string.Join('\n', lines); } private static string FormatParameters(ParameterInfo[] parameters) => string.Join( ", ", parameters.Select(static parameter => $"{FormatType(parameter.ParameterType)} {parameter.Name}")); private static string FormatType(Type type) { if (type.IsByRef) { return $"{FormatType(type.GetElementType()!)}&"; } if (type.IsArray) { return $"{FormatType(type.GetElementType()!)}[]"; } if (type.IsGenericType) { string name = type.GetGenericTypeDefinition().FullName!; name = name[..name.IndexOf('`')]; return $"{name}<{string.Join(",", type.GetGenericArguments().Select(FormatType))}>"; } return type.FullName ?? type.Name; } }