using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using UnitTestSharp;
namespace Vellum.Generator.UnitTests
{
public class SaveMethodsTests : TestFixture
{
public const string Header = "// \r\n" +
"// The generic methods Vellum.Generator found saving their type parameters, so their callers are " +
"checked.\r\n";
///
/// Returns the attributes generated, one per line.
///
public static string[] Lines(VellumRun run)
{
return run.SaveMethodsFile.Split(new[] { "\r\n" }, System.StringSplitOptions.RemoveEmptyEntries)
.Where(line => !line.StartsWith("//", System.StringComparison.Ordinal))
.ToArray();
}
public static string Attribute(string methodId, int typeParameter)
{
return "[assembly: global::Vellum.RegisterSaveMethod(\"" + methodId +
"\", typeof(global::Vellum.SaveMethodParameter" + typeParameter + "))]";
}
public const string Marker0 = SaveTemplateTests.Marker0;
public const string Marker1 = "global::Vellum.SaveMethodParameter1";
public const string ToNodeId = "M:Vellum.SaveSession.ToNode``1(``0)";
///
/// Returns what each call in hands on, as "callers => callees : arguments".
///
public static List Edges(string source)
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs", source));
SyntaxTree tree = compilation.SyntaxTrees[0];
SemanticModel model = compilation.GetSemanticModel(tree);
var returnMe = new List();
foreach (InvocationExpressionSyntax call in tree.GetRoot().DescendantNodes()
.OfType())
{
foreach (SaveEdge edge in SaveMethods.EdgesOf(model, call, CancellationToken.None))
{
string arguments = string.Join(", ", edge.Arguments.Items.Select(
argument => argument == null ? "null"
: argument.HasUnspellable ? "unspellable" : argument.ToSource()));
returnMe.Add(string.Join("|", edge.CallerIds.Items) + " => " +
string.Join("|", edge.CalleeIds.Items) + " : " + arguments);
}
}
return returnMe;
}
public class MightHoldEdgesTests : TestFixture
{
///
/// Returns whether each method declared in might hold edges.
///
public static List Might(string source)
{
SyntaxTree tree = CSharpSyntaxTree.ParseText(source);
return tree.GetRoot().DescendantNodes().OfType()
.Select(method => SaveMethods.MightHoldEdges(method, CancellationToken.None))
.ToList();
}
public void GenericMethod_Might()
{
CheckEqual(new[] { true }, Might("class C { void M(T value) { Save(value); } }"));
}
public void PlainMethod_Mightnt()
{
CheckEqual(new[] { false }, Might("class C { void M(int value) { Save(value); } }"));
}
public void MethodOfAGenericClass_Might()
{
CheckEqual(new[] { true }, Might("class C { void M(T value) { Save(value); } }"));
}
public void MethodOfAGenericStructRecordOrInterface_Might()
{
CheckEqual(new[] { true }, Might("struct S { void M(T value) { Save(value); } }"));
CheckEqual(new[] { true }, Might("record R { void M(T value) { Save(value); } }"));
CheckEqual(new[] { true }, Might("interface I { void M(T value) { Save(value); } }"));
}
public void MethodOfATypeNestedInAGenericOne_Might()
{
CheckEqual(new[] { true },
Might("class Outer { class Inner { void M(int value) { Save(value); } } }"));
}
public void MethodOfAGenericTypeNestedInAPlainOne_Might()
{
CheckEqual(new[] { true },
Might("class Outer { class Inner { void M(T value) { Save(value); } } }"));
}
public void MethodInANamespaceOfPlainTypes_Mightnt()
{
CheckEqual(new[] { false },
Might("namespace N { class Outer { class Inner { void M() { Save(1); } } } }"));
}
public void MethodNamingNothing_Mightnt()
{
CheckEqual(new[] { false }, Might("class C { void M(T first, U second) { } }"));
CheckEqual(new[] { false }, Might("class C { int M(T first) => 1; }"));
CheckEqual(new[] { false }, Might("abstract class C { public abstract void M(T first); }"));
}
public void MethodNamingAProperty_Might()
{
CheckEqual(new[] { true }, Might("class C { int M(C other) { return other.Count; } }"));
CheckEqual(new[] { true }, Might("class C { int M(C other) => other[0]; }"));
CheckEqual(new[] { true }, Might("class C { int M(C other) => other?[0] ?? 0; }"));
CheckEqual(new[] { true }, Might("class C { int M() => this[0]; }"));
}
public void CallInALambdaOrLocalFunction_Counts()
{
CheckEqual(new[] { true },
Might("class C { void M(T value) { System.Action act = () => Save(value); } }"));
CheckEqual(new[] { true }, Might("class C { void M(T value) { void L() { Save(value); } } }"));
}
public void ExpressionBodiedCall_Counts()
{
CheckEqual(new[] { true }, Might("class C { int M(T value) => Save(value); }"));
}
public void LocalFunction_Mightnt()
{
CSharpSyntaxNode local = CSharpSyntaxTree.ParseText("class C { void M() { void L() { } } }")
.GetRoot().DescendantNodes().OfType().Single();
CheckFalse(SaveMethods.MightHoldEdges(local, CancellationToken.None));
}
public void NodeThatIsNotAMethod_Mightnt()
{
CSharpSyntaxNode node = CSharpSyntaxTree.ParseText("class C { int field; C() { } }").GetRoot()
.DescendantNodes().OfType().Single();
CheckFalse(SaveMethods.MightHoldEdges(node, CancellationToken.None));
CheckFalse(SaveMethods.MightHoldEdges(node.Parent, CancellationToken.None));
}
///
/// Returns whether each accessor, and each expression-bodied property or indexer, in
/// might hold edges.
///
public static List MightAccessors(string source)
{
return CSharpSyntaxTree.ParseText(source).GetRoot().DescendantNodes()
.Where(node => node is AccessorDeclarationSyntax || node is ArrowExpressionClauseSyntax)
.Select(node => SaveMethods.MightHoldEdges(node, CancellationToken.None))
.ToList();
}
public void AccessorOfAGenericClass_Might()
{
CheckEqual(new[] { true, true }, MightAccessors(
"class C { T P { get { return Save(1); } set { Save(value); } } }"));
CheckEqual(new[] { true, true }, MightAccessors(
"class C { T this[int i] { get { return Save(i); } set { Save(value); } } }"));
CheckEqual(new[] { true, true }, MightAccessors(
"class C { event System.Action E { add { Save(1); } remove { Save(1); } } }"));
}
public void ExpressionBodiedPropertyOrIndexerOfAGenericClass_Might()
{
CheckEqual(new[] { true }, MightAccessors("class C { int P => Save(1); }"));
CheckEqual(new[] { true }, MightAccessors("class C { int this[int i] => Save(i); }"));
}
public void AccessorOfAPlainClass_Mightnt()
{
CheckEqual(new[] { false, false }, MightAccessors(
"class C { int P { get { return Save(1); } set { Save(value); } } }"));
CheckEqual(new[] { false }, MightAccessors("class C { int P => Save(1); }"));
}
public void AccessorWithNoBody_Mightnt()
{
CheckEqual(new[] { false, false }, MightAccessors("class C { T P { get; set; } }"));
CheckEqual(new[] { false }, MightAccessors("interface I { T P { get; } }"));
}
public void AccessorNamingNothing_Mightnt()
{
CheckEqual(new[] { false }, MightAccessors("class C { int P { get { return 1; } } }"));
CheckEqual(new[] { false }, MightAccessors("class C { int P => 1; }"));
}
public void ExpressionBodyOfAnAccessor_BelongsToTheAccessor()
{
CheckEqual(new[] { true, false }, MightAccessors("class C { int P { get => Save(1); } }"));
}
public void ExpressionBodyOfAMethod_IsNotAnAccessor()
{
CheckEqual(new[] { false }, MightAccessors("class C { int M() => Save(1); }"));
}
}
public class EdgesOfTests : TestFixture
{
public const string SaverPrefix = "public class Store { public SaveSession Session; ";
public void TypedToNodeOfTheParameter_HandsItOn()
{
CheckEqual(new[] { "M:Store.Put``1(``0) => " + ToNodeId + " : " + Marker0 },
Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(value); } }"));
}
public void TypeArgumentSpelled_HandsItOn()
{
CheckEqual(new[] { "M:Store.Put``1(``0) => " + ToNodeId + " : " + Marker0 },
Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(value); } }"));
}
public void TypeofOfTheParameter_HandsItOn()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0) => M:Vellum.SaveSession.ToNode(System.Object,System.Type) : " + Marker0,
}, Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(value, typeof(T)); } }"));
}
public void StaticCallee_HasNoReceiver()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0) => M:Vellum.SavedValue.ToNode(System.Object,System.Type) : " + Marker0,
}, Edges("public class Store { public void Put(T value) { " +
"SavedValue.ToNode(value, typeof(T)); } }"));
}
public void SecondTypeParameter_IsItsOwnStandIn()
{
CheckEqual(new[] { "M:Store.Pair``2(``0,``1) => " + ToNodeId + " : " + Marker1 },
Edges(SaverPrefix + "public void Pair(TKey key, T value) { Session.ToNode(value); } }"));
}
public void ParameterInsideAGenericType_IsKeptInside()
{
CheckEqual(new[]
{
"M:Store.Put``1(System.Collections.Generic.List{``0}) => " + ToNodeId +
" : global::System.Collections.Generic.List<" + Marker0 + ">",
}, Edges(SaverPrefix + "public void Put(List value) { Session.ToNode(value); } }"));
}
public void ParameterInsideArrays_IsKeptInside()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0[0:,0:][]) => " + ToNodeId + " : " + Marker0 + "[][,]",
}, Edges(SaverPrefix + "public void Put(T[][,] value) { Session.ToNode(value); } }"));
}
public void ParameterNextToAClosedType_KeepsTheClosedType()
{
CheckEqual(new[]
{
"M:Store.Put``1(System.Collections.Generic.Dictionary{``0,System.String}) => " + ToNodeId +
" : global::System.Collections.Generic.Dictionary<" + Marker0 + ", global::System.String>",
}, Edges(SaverPrefix + "public void Put(Dictionary value) { Session.ToNode(value); } }"));
}
public void ParameterInsideATypeNestedInAGenericOne_IsKeptInside()
{
CheckEqual(new[]
{
"M:Store.Put``1(Outer{System.Int32}.Inner{``0}) => " + ToNodeId +
" : global::Outer.Inner<" + Marker0 + ">",
}, Edges("public class Outer { public class Inner { } }\n" + SaverPrefix +
"public void Put(Outer.Inner value) { Session.ToNode(value); } }"));
}
public void CalleeGivenTwoTypeArguments_HandsOnEach()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0) => M:Saver.Pair``2(``0,``1) : global::System.Int32, " + Marker0,
}, Edges("public class Saver { public void Pair(A a, B b) { } }\n" +
"public class Store { public Saver Saver; " +
"public void Put(T value) { Saver.Pair(5, value); } }"));
}
public void ClosedTypesOnly_HandOnNothing()
{
CheckEqual(0, Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(5); " +
"Session.ToNode(\"text\"); } }").Count);
}
public void CallOutsideAGenericMethod_HandsOnNothing()
{
CheckEqual(0, Edges(SaverPrefix + "public void Put(int value) { Session.ToNode(value); } }").Count);
}
public const string ClassMarker0 = "global::Vellum.SaveClassParameter";
public const string ClassMarker1 = "global::Vellum.SaveClassParameter";
public void ParameterOfTheClass_IsHandedOn()
{
CheckEqual(new[] { "M:Store`1.Put``1(`0) => " + ToNodeId + " : " + ClassMarker0 },
Edges("public class Store { public SaveSession Session; " +
"public void Put(TValue value) { Session.ToNode(value); } }"));
}
public void ParameterOfAnOuterClass_IsNumberedBeforeThoseOfTheInnerOne()
{
CheckEqual(new[]
{
"M:Outer`1.Inner`1.Keep(`0) => " + ToNodeId + " : " + ClassMarker0,
"M:Outer`1.Inner`1.Put(`1) => " + ToNodeId + " : " + ClassMarker1,
}, Edges("public class Outer { public class Inner { public SaveSession Session; " +
"public void Keep(A value) { Session.ToNode(value); } " +
"public void Put(B value) { Session.ToNode(value); } } }"));
}
public void ParameterOfAnInnerClassOfAPlainOne_IsNumberedFromZero()
{
CheckEqual(new[] { "M:Outer.Inner`1.Put(`0) => " + ToNodeId + " : " + ClassMarker0 },
Edges("public class Outer { public class Inner { public SaveSession Session; " +
"public void Put(B value) { Session.ToNode(value); } } }"));
}
public void ClassAndMethodParametersTogether_AreEachSpelledByTheirOwnStandIn()
{
CheckEqual(new[]
{
"M:Store`1.Put``1(`0,``0) => " + ToNodeId + " : global::System.Collections.Generic.Dictionary<" +
ClassMarker0 + ", " + Marker0 + ">",
}, Edges("public class Store { public SaveSession Session; " +
"public void Put(TValue key, T value) { " +
"Session.ToNode(new Dictionary()); } }"));
}
public void ParameterOfTheClassInsideAType_IsKeptInside()
{
CheckEqual(new[]
{
"M:Store`1.Put(System.Collections.Generic.List{`0}) => " + ToNodeId +
" : global::System.Collections.Generic.List<" + ClassMarker0 + ">",
}, Edges("public class Store { public SaveSession Session; " +
"public void Put(List value) { Session.ToNode(value); } }"));
}
public void ParameterOfTheClassNextToAnUnspellableType_IsUnspellable()
{
CheckEqual(new[] { "M:Store`1.Put(`0) => " + ToNodeId + " : unspellable" },
Edges("public class Store { private class Hidden { } public SaveSession Session; " +
"public void Put(TValue value) { Session.ToNode(new Dictionary()); } }"));
}
public void ParameterOfTheClassInAMethodOfAPrivateClass_HandsOnNothing()
{
CheckEqual(0, Edges("public class Outer { private class Store { public SaveSession Session; " +
"public void Put(T value) { Session.ToNode(value); } } }").Count);
}
public void ClassParameterOfAnImplementedInterface_IsRegisteredForTheClassOnly()
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs",
"public interface IStore { void Put(X value); }\n" +
"public class Store : IStore { public SaveSession Session; " +
"public void Put(T value) { Session.ToNode(value); } }"));
SyntaxTree tree = compilation.SyntaxTrees[0];
InvocationExpressionSyntax call = tree.GetRoot().DescendantNodes()
.OfType().Single();
SaveEdge edge = SaveMethods.EdgesOf(compilation.GetSemanticModel(tree), call,
CancellationToken.None).Single();
CheckEqual(new[] { "M:Store`1.Put(`0)", "M:IStore`1.Put(`0)" }, edge.CallerIds.Items);
CheckEqual(1, edge.OwnCallerCount);
}
public void CallerThatIsOnlyAnInterfaceImplementation_HasNoOwnCaller()
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs",
"public interface IStore { void Put(T value); }\n" +
"public class Store : IStore { public SaveSession Session; " +
"void IStore.Put(T value) { Session.ToNode(value); } }"));
SyntaxTree tree = compilation.SyntaxTrees[0];
InvocationExpressionSyntax call = tree.GetRoot().DescendantNodes()
.OfType().Single();
SaveEdge edge = SaveMethods.EdgesOf(compilation.GetSemanticModel(tree), call,
CancellationToken.None).Single();
CheckEqual(new[] { "M:IStore.Put``1(``0)" }, edge.CallerIds.Items);
CheckEqual(0, edge.OwnCallerCount);
}
public void TypeAnAttributeCantSpell_IsLeftOutOfTheArguments()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0) => M:Saver.Pair``2(``0,``1) : null, " + Marker0,
}, Edges("public class Saver { public void Pair(A a, B b) { } }\n" +
"public class Store\n{\n private class Hidden { }\n public Saver Saver;\n" +
" public void Put(T value) { Saver.Pair(new Hidden(), value); }\n}"));
}
public void ArgumentThatIsntALiteralTypeof_IsLeftOutOfTheArguments()
{
CheckEqual(new[]
{
"M:Store.Put``1(System.Type) => M:Saver.Both(System.Type,System.Type) : null, " + Marker0,
}, Edges("public class Saver { public void Both(System.Type first, System.Type second) { } }\n" +
"public class Store { public Saver Saver; " +
"public void Put(System.Type other) { Saver.Both(other, typeof(T)); } }"));
}
public void NonTypeArguments_AreNotHandedOn()
{
CheckEqual(0, Edges("public class Saver { public void Note(string text) { } }\n" +
"public class Store { public Saver Saver; public void Put(T value) { Saver.Note(\"x\"); } }")
.Count);
}
public void ParameterOfALocalFunction_IsNotTheMethodsToHandOn()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0) => M:Saver.Pair``2(``0,``1) : " + Marker0 + ", null",
}, Edges("public class Saver { public void Pair(A a, B b) { } }\n" +
"public class Store { public Saver Saver; " +
"public void Put(T value) { void Inner(U other) { Saver.Pair(value, other); } } }"));
}
public void CallerInsideALocalFunction_IsTheOuterMethod()
{
CheckEqual(new[] { "M:Store.Put``1(``0) => " + ToNodeId + " : " + Marker0 },
Edges(SaverPrefix + "public void Put(T value) { void Inner() { Session.ToNode(value); } } }"));
}
public void CallerThatIsALocalFunction_HandsOnNothing()
{
CheckEqual(0, Edges(SaverPrefix + "public void Put() { void Inner(T value) " +
"{ Session.ToNode(value); } } }").Count);
}
public void CallerInALambda_IsTheEnclosingMethod()
{
CheckEqual(new[] { "M:Store.Put``1(``0) => " + ToNodeId + " : " + Marker0 },
Edges(SaverPrefix + "public void Put(T value) " +
"{ System.Action act = () => Session.ToNode(value); } }"));
}
public void CallerThatImplementsAnInterface_IsRegisteredUnderBoth()
{
CheckEqual(new[]
{
"M:Store.Put``1(``0)|M:IStore.Put``1(``0) => " + ToNodeId + " : " + Marker0,
}, Edges("public interface IStore { void Put(T value); }\n" +
SaverPrefix.Replace("Store", "Store : IStore") +
"public void Put(T value) { Session.ToNode(value); } }"));
}
public void CallerThatExplicitlyImplementsAnInterface_IsRegisteredUnderTheInterfaceOnly()
{
CheckEqual(new[] { "M:IStore.Put``1(``0) => " + ToNodeId + " : " + Marker0 },
Edges("public interface IStore { void Put(T value); }\n" +
SaverPrefix.Replace("Store", "Store : IStore") +
"void IStore.Put(T value) { Session.ToNode(value); } }"));
}
public void CallerInAPrivateType_HandsOnNothing()
{
CheckEqual(0, Edges("public class Outer { private class Store { public SaveSession Session; " +
"public void Put(T value) { Session.ToNode(value); } } }").Count);
}
public void CalleeCalledThroughAnInterface_IsNamedByItsInterfaceToo()
{
CheckEqual(new[]
{
"M:Store.Put``1(Bag,``0) => M:Bag.Add``1(``0)|M:IBag.Add``1(``0) : " + Marker0,
}, Edges("public interface IBag { void Add(T value); }\n" +
"public class Bag : IBag { public void Add(T value) { } }\n" +
"public class Store { public void Put(Bag bag, T value) { bag.Add(value); } }"));
}
public void NestingPastTheLimit_IsUnspellable()
{
string type = string.Concat(Enumerable.Repeat("List<", SaveTemplate.MaxDepth + 1)) + "T" +
new string('>', SaveTemplate.MaxDepth + 1);
CheckEqual(new[] { "M:Store.Put``1(" + string.Concat(Enumerable.Repeat(
"System.Collections.Generic.List{", SaveTemplate.MaxDepth + 1)) + "``0" +
new string('}', SaveTemplate.MaxDepth + 1) + ") => " + ToNodeId + " : unspellable" },
Edges(SaverPrefix + "public void Put(" + type + " value) { Session.ToNode(value); } }"));
}
public void TypeAnAttributeCantSpellAroundAParameter_IsUnspellable()
{
CheckEqual(new[]
{
"M:Store.Put``1(System.Collections.Generic.List{``0}) => " + ToNodeId + " : unspellable",
}, Edges("public class Store { private class Hidden { } public SaveSession Session; " +
"public void Put(List value) { Session.ToNode(new Dictionary()); } }"));
}
public void CallThatIsntAnInvocation_HandsOnNothing()
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs", "public class Plain { int x; }"));
SyntaxTree tree = compilation.SyntaxTrees[0];
CheckEqual(0, SaveMethods.EdgesOf(compilation.GetSemanticModel(tree), tree.GetRoot(),
CancellationToken.None).Length);
}
}
public class SolveTests : TestFixture
{
public const string TooDeep = "nests more than 32 types deep";
public static SaveTemplate Parameter(int number)
{
return SaveTemplate.OfParameter(number);
}
public static SaveTemplate Closed(string name)
{
return SaveTemplateTests.Concrete("global::" + name);
}
public static SaveRegistration Seed(string id, SaveTemplate template)
{
return new SaveRegistration(id, template);
}
public static SaveEdge Edge(string caller, string callee, params SaveTemplate[] arguments)
{
return new SaveEdge(SaveEdgeTests.Ids(caller), SaveEdgeTests.Ids(callee),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(arguments)));
}
public static List Solved(SaveRegistration[] seeds, SaveEdge[] edges,
List> dropped = null)
{
return SaveMethods.Solve(seeds, edges, dropped ?? new List>())
.Select(registration => registration.Id + "=" + registration.Template.ToSource()).ToList();
}
public void NoEdges_AddNothing()
{
CheckEqual(0, Solved(new[] { Seed("A", Parameter(0)) }, new SaveEdge[0]).Count);
}
public void CalleeNothingRegistered_AddsNothing()
{
CheckEqual(0, Solved(new[] { Seed("A", Parameter(0)) }, new[] { Edge("C", "B", Parameter(0)) }).Count);
}
public void RegisteredCallee_RegistersTheCaller()
{
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("A", Parameter(0)) }, new[] { Edge("C", "A", Parameter(0)) }));
}
public void TemplateOfTheCallee_IsFilledFromWhatTheCallerHandsOn()
{
SaveTemplate handed = SaveTemplateTests.Concrete("global::Pair<{0}, {1}>", Parameter(0), Closed("S"));
CheckEqual(new[]
{
"C=global::System.Collections.Generic.List>",
}, Solved(new[] { Seed("A", SaveTemplateTests.ListOf(Parameter(0))) },
new[] { Edge("C", "A", handed) }));
}
public void SecondTypeParameter_TakesTheSecondArgument()
{
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("A", Parameter(1)) }, new[] { Edge("C", "A", Closed("S"), Parameter(0)) }));
}
public void ArgumentThatCantBeSpelled_AddsNothing()
{
CheckEqual(0, Solved(new[] { Seed("A", Parameter(0)) }, new[] { Edge("C", "A", (SaveTemplate)null) })
.Count);
}
public void ClosedArgument_AddsNothing()
{
CheckEqual(0, Solved(new[] { Seed("A", Parameter(0)) }, new[] { Edge("C", "A", Closed("S")) }).Count);
}
public void TemplateNamingAParameterTheEdgeLacks_AddsNothing()
{
CheckEqual(0, Solved(new[] { Seed("A", Parameter(3)) }, new[] { Edge("C", "A", Parameter(0)) }).Count);
}
public void EveryCallerId_IsRegistered()
{
var edge = new SaveEdge(SaveEdgeTests.Ids("C", "IC"), SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(Parameter(0))));
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0, "IC=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }));
}
public void EveryCalleeId_IsLookedUp()
{
var edge = new SaveEdge(SaveEdgeTests.Ids("C"), SaveEdgeTests.Ids("Other", "A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(Parameter(0))));
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }));
}
public void ChainOfCallers_IsFollowedInAnyOrder()
{
CheckEqual(new[]
{
"B=" + SaveTemplateTests.Marker0, "C=" + SaveTemplateTests.Marker0,
"D=" + SaveTemplateTests.Marker0,
}, Solved(new[] { Seed("A", Parameter(0)) },
new[] { Edge("D", "C", Parameter(0)), Edge("C", "B", Parameter(0)), Edge("B", "A", Parameter(0)) })
.OrderBy(line => line, System.StringComparer.Ordinal).ToList());
}
public void MethodsThatCallEachOther_EachGetRegisteredOnce()
{
CheckEqual(new[] { "A=" + SaveTemplateTests.Marker0, "B=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("Saver", Parameter(0)) },
new[]
{
Edge("A", "Saver", Parameter(0)), Edge("A", "B", Parameter(0)),
Edge("B", "A", Parameter(0)),
}).OrderBy(line => line, System.StringComparer.Ordinal).ToList());
}
public void TemplateAlreadyKnown_IsNotRegisteredAgain()
{
CheckEqual(0, Solved(new[] { Seed("A", Parameter(0)), Seed("C", Parameter(0)) },
new[] { Edge("C", "A", Parameter(0)) }).Count);
}
public void MethodHandingAGrowingTypeToItself_StopsWhereTheTypeGetsTooDeep()
{
var dropped = new List>();
List solved = Solved(new[] { Seed("G", Parameter(0)) },
new[] { Edge("G", "G", SaveTemplateTests.ListOf(Parameter(0))) }, dropped);
CheckEqual(SaveTemplate.MaxDepth - 1, solved.Count);
CheckEqual(new[] { new KeyValuePair("G", TooDeep) }, dropped);
}
public void UnspellableArgumentTheCalleeSaves_IsDroppedAndNotRegistered()
{
var dropped = new List>();
List solved = Solved(new[] { Seed("A", Parameter(0)) },
new[] { Edge("C", "A", SaveTemplate.Unspellable) }, dropped);
CheckEqual(0, solved.Count);
CheckEqual(new[] { new KeyValuePair("C", SaveMethods.UnspellableReason) }, dropped);
}
public void UnspellableArgumentInsideTheTemplate_IsDropped()
{
var dropped = new List>();
List solved = Solved(new[] { Seed("A", SaveTemplateTests.ListOf(Parameter(0))) },
new[] { Edge("C", "A", SaveTemplate.Unspellable) }, dropped);
CheckEqual(0, solved.Count);
CheckEqual(1, dropped.Count);
}
public void UnspellableArgumentTheCalleeDoesntSave_LeavesTheRestRegistered()
{
var dropped = new List>();
List solved = Solved(new[] { Seed("A", Parameter(1)) },
new[] { Edge("C", "A", SaveTemplate.Unspellable, Parameter(0)) }, dropped);
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0 }, solved);
CheckEqual(0, dropped.Count);
}
public static SaveEdge WithClassArguments(string[] callers, int own,
EquatableArray> maps, string[] callees,
EquatableArray> classArguments, params SaveTemplate[] arguments)
{
return new SaveEdge(SaveEdgeTests.Ids(callers), own, maps, SaveEdgeTests.Ids(callees), classArguments,
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(arguments)));
}
public void CalleeClassArgument_FillsTheCalleesClassParameter()
{
SaveEdge edge = WithClassArguments(new[] { "C" }, 1, default, new[] { "A" },
SaveEdgeTests.ClassArguments(new[] { Parameter(0) }));
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("A", SaveTemplate.OfClassParameter(0)) }, new[] { edge }));
}
public void CalleeClassArgumentMadeOfSeveralParts_IsFilledInPlace()
{
SaveEdge edge = WithClassArguments(new[] { "C" }, 1, default, new[] { "A" },
SaveEdgeTests.ClassArguments(new[] { SaveTemplateTests.ListOf(Parameter(0)), Closed("S") }));
CheckEqual(new[]
{
"C=global::System.Collections.Generic.Dictionary, global::S>",
}, Solved(new[]
{
Seed("A", SaveTemplateTests.Concrete("global::System.Collections.Generic.Dictionary<{0}, {1}>",
SaveTemplate.OfClassParameter(0), SaveTemplate.OfClassParameter(1))),
}, new[] { edge }));
}
public void EachCalleeIsFilledFromItsOwnClassArguments()
{
SaveEdge edge = WithClassArguments(new[] { "C" }, 1, default, new[] { "A", "IA" },
SaveEdgeTests.ClassArguments(new[] { Parameter(0) }, new[] { Parameter(1) }));
CheckEqual(new[] { "C=" + SaveMethodsTests.Marker1 },
Solved(new[] { Seed("IA", SaveTemplate.OfClassParameter(0)) }, new[] { edge }));
}
public void CalleeWithNoClassArguments_LeavesItsClassParameterUnfilled()
{
SaveEdge edge = WithClassArguments(new[] { "C" }, 1, default, new[] { "A" }, default, Parameter(0));
CheckEqual(0, Solved(new[] { Seed("A", SaveTemplate.OfClassParameter(0)) }, new[] { edge }).Count);
}
public void ClassArgumentThatCantBeSpelled_DropsTheCaller()
{
SaveEdge edge = WithClassArguments(new[] { "C" }, 1, default, new[] { "A" },
SaveEdgeTests.ClassArguments(new[] { SaveTemplate.Unspellable }));
var dropped = new List>();
List solved = Solved(new[] { Seed("A", SaveTemplate.OfClassParameter(0)) },
new[] { edge }, dropped);
CheckEqual(0, solved.Count);
CheckEqual(new[] { new KeyValuePair("C", SaveMethods.UnspellableReason) }, dropped);
}
public void ClassTemplateUnderAnInterface_IsRenumberedByThatInterface()
{
SaveEdge edge = WithClassArguments(new[] { "C", "IC" }, 1,
SaveEdgeTests.Maps(new int[0], new[] { -1, 0 }), new[] { "A" },
SaveEdgeTests.ClassArguments(new[] { SaveTemplate.OfClassParameter(1) }));
CheckEqual(new[]
{
"C=" + SaveTemplate.OfClassParameter(1).ToSource(),
"IC=" + SaveTemplate.OfClassParameter(0).ToSource(),
}, Solved(new[] { Seed("A", SaveTemplate.OfClassParameter(0)) }, new[] { edge }));
}
public void ClassTemplateUnderAnInterfaceThatDoesntTakeTheParameter_DropsThatIdAlone()
{
SaveEdge edge = WithClassArguments(new[] { "C", "IC1", "IC2" }, 1,
SaveEdgeTests.Maps(new int[0], new[] { -1 }, new[] { 0 }), new[] { "A" },
SaveEdgeTests.ClassArguments(new[] { SaveTemplate.OfClassParameter(0) }));
var dropped = new List>();
List solved = Solved(new[] { Seed("A", SaveTemplate.OfClassParameter(0)) },
new[] { edge }, dropped);
CheckEqual(new[]
{
"C=" + SaveTemplate.OfClassParameter(0).ToSource(),
"IC2=" + SaveTemplate.OfClassParameter(0).ToSource(),
}, solved);
CheckEqual(new[] { new KeyValuePair("IC1", SaveMethods.InheritedReason) }, dropped);
}
public void ClassTemplateUnderAnInterfaceWithNoMap_IsDroppedWithTheInheritedReason()
{
var dropped = new List>();
var edge = new SaveEdge(SaveEdgeTests.Ids("C", "IC"), 1, SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(
SaveTemplate.OfClassParameter(0))));
Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }, dropped);
CheckEqual(new[] { new KeyValuePair("IC", SaveMethods.InheritedReason) }, dropped);
}
public void MixedTemplateUnderAnInterface_RenumbersTheClassPartAndKeepsTheMethodPart()
{
SaveTemplate pair = SaveTemplateTests.Concrete("global::Pair<{0}, {1}>",
SaveTemplate.OfClassParameter(1), Parameter(0));
SaveEdge edge = WithClassArguments(new[] { "C", "IC" }, 1,
SaveEdgeTests.Maps(new int[0], new[] { -1, 0 }), new[] { "A" }, default, pair);
CheckEqual(new[]
{
"C=global::Pair<" + SaveTemplate.OfClassParameter(1).ToSource() + ", " + SaveTemplateTests.Marker0 +
">",
"IC=global::Pair<" + SaveTemplate.OfClassParameter(0).ToSource() + ", " +
SaveTemplateTests.Marker0 + ">",
}, Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }));
}
public void MethodTemplateUnderAnInterface_NeedsNoMap()
{
var edge = new SaveEdge(SaveEdgeTests.Ids("C", "IC"), 1, SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(Parameter(0))));
var dropped = new List>();
List solved = Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }, dropped);
CheckEqual(2, solved.Count);
CheckEqual(0, dropped.Count);
}
public void ClassTemplate_IsRegisteredUnderTheCallersOwnIdOnly()
{
var edge = new SaveEdge(SaveEdgeTests.Ids("C", "IC"), 1, SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(
SaveTemplate.OfClassParameter(0))));
CheckEqual(new[] { "C=" + EdgesOfTests.ClassMarker0 },
Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }));
}
public void ClassTemplateOfACallerWithNoOwnId_IsRegisteredNowhere()
{
var edge = new SaveEdge(SaveEdgeTests.Ids("IC"), 0, SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(
SaveTemplate.OfClassParameter(0))));
CheckEqual(0, Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }).Count);
}
public void MethodTemplate_IsRegisteredUnderEveryCallerId()
{
var edge = new SaveEdge(SaveEdgeTests.Ids("C", "IC"), 1, SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(
Parameter(0))));
CheckEqual(new[] { "C=" + SaveTemplateTests.Marker0, "IC=" + SaveTemplateTests.Marker0 },
Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }));
}
public void TemplateMixingClassAndMethodParameters_IsRegisteredUnderTheOwnIdOnly()
{
SaveTemplate pair = SaveTemplateTests.Concrete("global::Pair<{0}, {1}>",
SaveTemplate.OfClassParameter(0), Parameter(0));
var edge = new SaveEdge(SaveEdgeTests.Ids("C", "IC"), 1, SaveEdgeTests.Ids("A"),
new EquatableArray(System.Collections.Immutable.ImmutableArray.Create(pair)));
CheckEqual(1, Solved(new[] { Seed("A", Parameter(0)) }, new[] { edge }).Count);
}
public void CalleeTemplateOfAClassParameter_IsNotComposedYet()
{
CheckEqual(0, Solved(new[] { Seed("A", SaveTemplate.OfClassParameter(0)) },
new[] { Edge("C", "A", Parameter(0)) }).Count);
}
public void ParameterPastTheNinth_IsRegisteredByItsDigits()
{
CheckEqual(new[]
{
"C=global::Vellum.SaveMethodParameter",
}, Solved(new[] { Seed("A", Parameter(0)) }, new[] { Edge("C", "A", Parameter(12)) }));
}
}
public class VellumsOwnRegistrationsTests : TestFixture
{
public void EachOne_NamesAMethodThatExists()
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs", ""));
var missing = new List();
foreach (SaveRegistration seed in SaveMethods.SeedsOf(compilation).Items)
{
if (seed.Id.StartsWith("M:Vellum.", System.StringComparison.Ordinal)
&& DocumentationCommentId.GetFirstSymbolForDeclarationId(seed.Id, compilation) == null)
{
missing.Add(seed.Id);
}
}
CheckEqual(0, missing.Count);
}
public void EachOne_IsRegisteredOnceForEachTypeItSaves()
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs", ""));
var seen = new HashSet();
foreach (SaveRegistration seed in SaveMethods.SeedsOf(compilation).Items)
{
CheckTrue(seen.Add(seed.Id + "=" + seed.Template.ToSource()));
}
}
}
public class SeedsOfTests : TestFixture
{
public static List Seeds(string source)
{
return SaveMethods.SeedsOf(VellumRun.Compile(("Source.cs", source))).Items
.Where(seed => !seed.Id.StartsWith("M:Vellum.", System.StringComparison.Ordinal))
.Select(seed => seed.Id + "=" + seed.Template.ToSource()).ToList();
}
public void Registrations_AreSortedByIdThenType()
{
CheckEqual(new[]
{
"M:A.Put``1(``0)=global::System.Collections.Generic.List<" + SaveTemplateTests.Marker0 + ">",
"M:A.Put``1(``0)=" + SaveTemplateTests.Marker0,
"M:B.Put``1(``0)=" + SaveTemplateTests.Marker0,
}, Seeds(@"
[assembly: RegisterSaveMethod(""M:B.Put``1(``0)"", typeof(SaveMethodParameter0))]
[assembly: RegisterSaveMethod(""M:A.Put``1(``0)"", typeof(SaveMethodParameter0))]
[assembly: RegisterSaveMethod(""M:A.Put``1(``0)"", typeof(List))]"));
}
public void RegistrationOfATypeThatCantBeSpelled_IsLeftOut()
{
CheckEqual(0, Seeds(@"
public class Outer { private class Hidden { } }
[assembly: RegisterSaveMethod(""M:A.Put``1(``0)"", typeof(Missing))]").Count);
}
public void RegistrationInAReference_IsIncluded()
{
CSharpCompilation compilation = VellumRun.Compile(new[] { SavedTypeAnalyzerTests.StoreLibrary },
("Source.cs", ""));
CheckTrue(SaveMethods.SeedsOf(compilation).Items.Any(
seed => seed.Id == "M:IStore.Put``1(System.String,``0)"));
}
public void SameCompilation_GivesEqualSeeds()
{
CSharpCompilation compilation = VellumRun.Compile(("Source.cs", "public class Plain { }"));
CheckEqual(SaveMethods.SeedsOf(compilation), SaveMethods.SeedsOf(compilation));
}
}
public class AttributesTests : TestFixture
{
public void EachRegistration_IsAnAttributeSpellingItsTemplate()
{
var registrations = new[]
{
new SaveRegistration("M:A.Put``1(``0)", SaveTemplateTests.ListOf(SaveTemplate.OfParameter(0))),
};
CheckEqual(new[]
{
"[assembly: global::Vellum.RegisterSaveMethod(\"M:A.Put``1(``0)\", typeof(global::System." +
"Collections.Generic.List))]",
}, SaveMethods.Attributes(registrations));
}
}
public class PipelineTests : TestFixture
{
public const string Store = "public class Store { public SaveSession Session; " +
"public void Put(T value) { Session.ToNode(value); } }\n";
public void WrapperOfASavingMethod_IsRegistered()
{
VellumRun run = VellumRun.Of(Store + @"
public class Front { public Store Store; public void Save(T value) { Store.Put(value); } }");
CheckEqual(0, run.Compiled.Count);
CheckEqual(new[]
{
Attribute("M:Front.Save``1(``0)", 0), Attribute("M:Store.Put``1(``0)", 0),
}, Lines(run));
}
public void WrapperOfAWrapper_IsRegistered()
{
VellumRun run = VellumRun.Of(Store + @"
public class Front { public Store Store; public void Save(T value) { Store.Put(value); } }
public class Back { public Front Front; public void Keep(T value) { Front.Save(value); } }");
CheckEqual(new[]
{
Attribute("M:Back.Keep``1(``0)", 0), Attribute("M:Front.Save``1(``0)", 0),
Attribute("M:Store.Put``1(``0)", 0),
}, Lines(run));
}
public void MethodOfAGenericClass_IsRegisteredByItsClassParameter()
{
VellumRun run = VellumRun.Of(@"
public class Store
{
public SaveSession Session;
public void Put(T value) { Session.ToNode(value); }
}");
CheckEqual(0, run.Compiled.Count);
CheckEqual(new[]
{
"[assembly: global::Vellum.RegisterSaveMethod(\"M:Store`1.Put(`0)\", typeof(global::Vellum." +
"SaveClassParameter))]",
}, Lines(run));
}
public const string ClassStandIn0 =
"typeof(global::Vellum.SaveClassParameter)";
public const string ClassStandIn1 =
"typeof(global::Vellum.SaveClassParameter)";
public static string ClassAttribute(string id, string standIn)
{
return "[assembly: global::Vellum.RegisterSaveMethod(\"" + id + "\", " + standIn + ")]";
}
public void ClassParameterTemplate_IsRegisteredUnderAnInterfaceByTheInterfacesNumber()
{
VellumRun run = VellumRun.Of(@"
public interface IStore { void Put(X value); }
public class Store : IStore
{
public SaveSession Session;
public void Put(T value) { Session.ToNode(value); }
}");
CheckEqual(new[]
{
ClassAttribute("M:IStore`1.Put(`0)", ClassStandIn0),
ClassAttribute("M:Store`1.Put(`0)", ClassStandIn0),
}, Lines(run));
CheckEqual(0, run.Reported.Count);
}
public void InterfaceThatTakesTheClassesParametersInAnotherOrder_IsRegisteredWithItsOwnNumbers()
{
VellumRun run = VellumRun.Of(@"
public interface IBag { void Put(X value); }
public class Pair : IBag
{
public SaveSession Session;
public void Put(B value) { Session.ToNode(value); }
}");
CheckEqual(new[]
{
ClassAttribute("M:IBag`1.Put(`0)", ClassStandIn0),
ClassAttribute("M:Pair`2.Put(`1)", ClassStandIn1),
}, Lines(run));
}
public void InterfaceInsideATypeNestedInAGenericOne_IsRegisteredWithItsOwnNumbers()
{
VellumRun run = VellumRun.Of(@"
public interface IBag { void Put(X value); }
public class Outer
{
public class Inner : IBag
{
public SaveSession Session;
public void Put(B value) { Session.ToNode(value); }
}
}");
CheckEqual(new[]
{
ClassAttribute("M:IBag`1.Put(`0)", ClassStandIn0),
ClassAttribute("M:Outer`1.Inner`1.Put(`1)", ClassStandIn1),
}, Lines(run));
}
public void InterfaceThatTakesTheSameParameterTwice_IsRegisteredWithItsFirstNumber()
{
VellumRun run = VellumRun.Of(@"
public interface IBag { void Put(X value); }
public class Twin : IBag
{
public SaveSession Session;
public void Put(A value) { Session.ToNode(value); }
}");
CheckEqual(new[]
{
ClassAttribute("M:IBag`2.Put(`0)", ClassStandIn0),
ClassAttribute("M:Twin`1.Put(`0)", ClassStandIn0),
}, Lines(run));
}
public void InterfaceThatTakesTheParameterInsideAnotherType_IsNotRegisteredAndWarns()
{
VellumRun run = VellumRun.Of(@"
public interface IBag { void Put(X value); }
public class Wrap : IBag>
{
public SaveSession Session;
public void Put(List value) { Session.ToNode(value); }
}");
CheckEqual(new[]
{
"[assembly: global::Vellum.RegisterSaveMethod(" +
"\"M:Wrap`1.Put(System.Collections.Generic.List{`0})\", " +
"typeof(global::System.Collections.Generic.List>))]",
}, Lines(run));
CheckEqual(new[] { "VEL0016" }, run.Reported);
}
public void MethodImplementingAnInterfaceExplicitly_IsRegisteredUnderTheInterfaceOnly()
{
VellumRun run = VellumRun.Of(@"
public interface IBag { void Put(X value); }
public class Pair : IBag
{
public SaveSession Session;
void IBag.Put(B value) { Session.ToNode(value); }
}");
CheckEqual(new[] { ClassAttribute("M:IBag`1.Put(`0)", ClassStandIn0) }, Lines(run));
}
public void CallThroughTheInterface_IsCheckedWithTheInterfacesArgument()
{
MetadataReference library = VellumRun.CompileLibrary("Saver", @"
public interface IBag { void Put(string key, X value); }
public class Pair : IBag
{
public SaveSession Session;
public void Put(string key, B value) { Session.ToNode(value); }
}");
VellumRun run = VellumRun.Of(library, @"
public class User
{
public void Run(IBag> bad, IBag good,
Pair