Skip to content

Commit dcdca50

Browse files
authored
Merge pull request #17 from in-async/release/v0.4.0
Release/v0.4.0 * Fix #15 ターゲット型がインターフェースの場合に継承メンバーを比較しない不具合を修正。 * ターゲット型に同名のデータメンバーが存在した場合、一意に解決できない為 CS0229 相当とみなし、ArgumentException をスローするよう対応。 cf. #15 (comment)
2 parents 4d574e3 + 3037f6b commit dcdca50

11 files changed

Lines changed: 183 additions & 27 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
4+
namespace Inasync.Tests {
5+
6+
[TestClass]
7+
public class IssueTests {
8+
9+
[TestMethod]
10+
public void Issue15_Test() => Issue15.Test();
11+
12+
private static class Issue15 {
13+
14+
public static void Test() {
15+
IDerived x = new Derived { V0 = 0, V1 = 1 };
16+
17+
Action TestCase(int testNo, object y, Type? expectedException = null) => () => {
18+
TestAA
19+
.Act(() => x.AssertIs(y))
20+
.Assert(expectedException, message: $"No.{testNo}");
21+
};
22+
23+
new[] {
24+
TestCase( 0, y: new { V1 = 1 }, expectedException: typeof(PrimitiveAssertFailedException)), // y が IDerived のデータメンバーを全て実装していないので失敗すべき。
25+
TestCase( 1, y: new { V0 = 0, V1 = 1 }),
26+
}.Invoke();
27+
}
28+
29+
private interface IBase {
30+
int V0 { get; }
31+
}
32+
33+
private interface IDerived : IBase {
34+
int V1 { get; }
35+
}
36+
37+
private class Derived : IDerived {
38+
public int V0 { get; set; }
39+
public int V1 { get; set; }
40+
}
41+
}
42+
43+
[TestMethod]
44+
public void Issue15_2_Test() => Issue15_2.Test();
45+
46+
private static class Issue15_2 {
47+
48+
public static void Test() {
49+
var x = new FooBar(foo: 1, bar: 2, value: "foo bar", fooValue: "foo", barValue: "bar");
50+
51+
static Action TestCase<T>(int testNo, T x, object y, Type? expectedException = null) => () => {
52+
TestAA
53+
.Act(() => x.AssertIs(y))
54+
.Assert(expectedException, message: $"No.{testNo}");
55+
};
56+
57+
new[] {
58+
TestCase( 0, (FooBar)x , y: new { Foo = 1, Bar = 2, Value = "foo bar" }),
59+
TestCase( 1, (IFoo)x , y: new { Foo = 1, Value = "foo" }),
60+
TestCase( 2, (IBar)x , y: new { Bar = 2, Value = "bar" }),
61+
TestCase( 3, (IFooBar)x, y: new { Foo = 1, Bar = 2, Value = "foo bar" }, expectedException: typeof(ArgumentException)), // CS0229 相当: IFoo.Value と IBar.Value 間があいまい
62+
TestCase( 4, (IFooBar)x, y: new FooBar(foo: 1, bar: 2, value: "foo bar", fooValue: "foo", barValue: "bar"), expectedException: typeof(ArgumentException)), // 実体が等価に見えても、ターゲット型 IFooBar の Value があいまいなまま
63+
TestCase( 5, (IFooBar)x, y: x), // 参照等価であれば、IFooBar.Value のあいまいさを解決する必要が無いので、成功
64+
}.Invoke();
65+
}
66+
67+
private interface IFoo {
68+
int Foo { get; }
69+
string Value { get; }
70+
}
71+
72+
private interface IBar {
73+
int Bar { get; }
74+
string Value { get; }
75+
}
76+
77+
private interface IFooBar : IFoo, IBar { }
78+
79+
private sealed class FooBar : IFooBar {
80+
private readonly string _fooValue;
81+
private readonly string _barValue;
82+
83+
public FooBar(int foo, int bar, string value, string fooValue, string barValue) {
84+
Foo = foo;
85+
Bar = bar;
86+
Value = value;
87+
_fooValue = fooValue;
88+
_barValue = barValue;
89+
}
90+
91+
public int Foo { get; }
92+
public int Bar { get; }
93+
public string Value { get; }
94+
string IFoo.Value => _fooValue;
95+
string IBar.Value => _barValue;
96+
}
97+
}
98+
}
99+
}

Inasync.PrimitiveAssert.Tests/PrimitiveAssertTests_AssertIs.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ Action TestCase<T>(int testNo, T x, object y, Type? expectedException = null) =>
187187
TestCase( 5, x: new Dictionary<int, int>{ { 1, 2 } }, y: new Dictionary<int, int>{ { 1, 2 } }), // issue #8: Dictionary<> のような型引数と要素型が一致しないコレクションでも、各要素を要素型で等値アサートできる。
188188
TestCase( 6, x: new CirculatedClass() , y: new CirculatedClass() ), // issue #9: 循環参照しているデータ メンバーは循環先のパスで比較。
189189
TestCase( 7, x: new { Obj = new CirculatedClass() } , y: new { Obj = new CirculatedClass() } ), // issue #9: 循環参照しているデータ メンバーは循環先のパスで比較。
190+
TestCase( 8, x: (IBase) new Derived{ V0 = 0, V1 = 1 }, y: new { V0 = 0 }),
191+
TestCase( 9, x: (IDerived)new Derived{ V0 = 0, V1 = 1 }, y: new { V0 = 0 }, expectedException: typeof(PrimitiveAssertFailedException)), // issue #15: expected が IDerived のデータメンバーを全て実装していないので失敗すべき。
192+
TestCase(10, x: (IDerived)new Derived{ V0 = 0, V1 = 1 }, y: new { V1 = 1 }, expectedException: typeof(PrimitiveAssertFailedException)), // issue #15: expected が IDerived のデータメンバーを全て実装していないので失敗すべき。
193+
TestCase(11, x: (IDerived)new Derived{ V0 = 0, V1 = 1 }, y: new { V0 = 0, V1 = 1 }),
194+
TestCase(12, x: new{ Foo=1 }, y: new{ Foo=1m }),
195+
TestCase(13, x: new{ Foo=1 }, y: new{ Foo=2m }, expectedException: typeof(PrimitiveAssertFailedException)),
190196
}.Invoke();
191197
}
192198

@@ -215,6 +221,19 @@ private class CirculatedClass {
215221
public CirculatedClass Self => this;
216222
}
217223

224+
private interface IBase {
225+
int V0 { get; }
226+
}
227+
228+
private interface IDerived : IBase {
229+
int V1 { get; }
230+
}
231+
232+
private sealed class Derived : IDerived {
233+
public int V0 { get; set; }
234+
public int V1 { get; set; }
235+
}
236+
218237
#endregion Helper
219238
}
220239
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.Generic;
2+
3+
namespace Inasync {
4+
5+
internal static class DictionaryExtensions {
6+
7+
public static bool TryRemove<TKey, TValue>(this IDictionary<TKey, TValue> source, TKey key, out TValue value) {
8+
if (!source.TryGetValue(key, out value)) { return false; }
9+
source.Remove(key);
10+
return true;
11+
}
12+
}
13+
}

Inasync.PrimitiveAssert/Commons/TypeExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Reflection;
45

56
namespace Commons {
67

@@ -38,5 +39,21 @@ public static bool IsNullable(this Type type) {
3839

3940
return null;
4041
}
42+
43+
/// <summary>
44+
/// 指定された型が備えるプロパティを全て返します。
45+
/// <paramref name="type"/> がインターフェースの場合は、そのインターフェースが実装する全てのインターフェースのプロパティも含みます。
46+
/// </summary>
47+
/// <remarks>https://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy/26766221#26766221</remarks>
48+
public static IEnumerable<PropertyInfo> GetPropertiesEx(this Type type, BindingFlags bindingAttr) {
49+
if (!type.IsInterface) {
50+
return type.GetProperties(bindingAttr);
51+
}
52+
53+
return new[] { type }
54+
.Concat(type.GetInterfaces())
55+
.SelectMany(x => x.GetProperties(bindingAttr))
56+
;
57+
}
4158
}
4259
}

Inasync.PrimitiveAssert/Inasync.PrimitiveAssert.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<PackageProjectUrl>https://github.com/in-async/PrimitiveAssert</PackageProjectUrl>
1010
<PackageLicenseUrl>https://github.com/in-async/PrimitiveAssert/blob/master/LICENSE</PackageLicenseUrl>
1111
<PackageTags>library test unittest assert deep</PackageTags>
12-
<Version>0.3.4</Version>
12+
<Version>0.4.0</Version>
1313
</PropertyGroup>
1414

1515
</Project>

Inasync.PrimitiveAssert/Internals/AssertIsImpl.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using System.Linq;
34
using Commons;
45

56
namespace Inasync {
@@ -13,6 +14,7 @@ public AssertIsImpl(string? message, Action<string>? logger) {
1314
_logger = logger;
1415
}
1516

17+
/// <exception cref="ArgumentException">ターゲット型に同じ名前のデータメンバーが 2 つ以上存在します。</exception>
1618
public void AssertIs(AssertNode node) {
1719
var targetType = node.TargetType;
1820
var actual = node.Actual;
@@ -131,6 +133,7 @@ private bool TryCollectionAssertIs(Type targetType, object actual, object expect
131133
return false;
132134
}
133135

136+
/// <exception cref="ArgumentException">ターゲット型に同じ名前のデータメンバーが 2 つ以上存在します。</exception>
134137
private bool TryCompositeAssertIs(Type targetType, object actual, object expected, AssertNode node) {
135138
targetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
136139

@@ -151,10 +154,21 @@ private bool TryCompositeAssertIs(Type targetType, object actual, object expecte
151154
return true;
152155
}
153156

157+
// ターゲット型に同じ名前のデータメンバーが含まれていない事をチェック。
158+
// 同じ名前のデータメンバーが 2 つ以上存在する場合、アサート対象を解決できない為 (CS0229 相当)。
159+
var targetDataMembers = targetType.GetDataMembers().ToArray();
160+
var duplicateMember = targetDataMembers.ToLookup(x => x.Name).FirstOrDefault(g => g.Count() > 1);
161+
if (duplicateMember != null) {
162+
var duplicateMemberNames = duplicateMember.Select(x => $"'{x.DeclaringType.Name}.{x.Name}'");
163+
throw new ArgumentException(message: $"ターゲット型 '{targetType.Name}' に含まれる {string.Join(" と ", duplicateMemberNames)} があいまいです。");
164+
}
165+
154166
// 各データ メンバーの比較
155-
foreach (var member in targetType.GetDataMembers()) {
156-
if (!actualType.TryGetDataMember(member.Name, out var actualMember)) { throw new PrimitiveAssertFailedException(node, $"actual にデータ メンバー {member.Name} が見つかりません。", _message); }
157-
if (!expectedType.TryGetDataMember(member.Name, out var expectedMember)) { throw new PrimitiveAssertFailedException(node, $"expected にデータ メンバー {member.Name} が見つかりません。", _message); }
167+
var actualMemberMap = actualType.GetDataMembers().ToDictionary(x => x.Name);
168+
var expectedMemberMap = expectedType.GetDataMembers().ToDictionary(x => x.Name);
169+
foreach (var member in targetDataMembers) {
170+
if (!actualMemberMap.TryRemove(member.Name, out var actualMember)) { throw new PrimitiveAssertFailedException(node, $"actual にデータ メンバー {member.Name} が見つかりません。", _message); }
171+
if (!expectedMemberMap.TryRemove(member.Name, out var expectedMember)) { throw new PrimitiveAssertFailedException(node, $"expected にデータ メンバー {member.Name} が見つかりません。", _message); }
158172

159173
var actualMemberValue = actualMember.GetValue(actual);
160174
var expectedMemberValue = expectedMember.GetValue(expected);

Inasync.PrimitiveAssert/Internals/DataMember.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@ internal readonly struct DataMember {
1212

1313
public readonly string Name;
1414
public readonly Type DataType;
15+
public readonly Type DeclaringType;
1516

1617
public DataMember(PropertyInfo prop) {
1718
_prop = prop;
1819
_field = null;
1920
Name = prop.Name;
2021
DataType = prop.PropertyType;
22+
DeclaringType = prop.DeclaringType;
2123
}
2224

2325
public DataMember(FieldInfo field) {
2426
_prop = null;
2527
_field = field;
2628
Name = field.Name;
2729
DataType = field.FieldType;
30+
DeclaringType = field.DeclaringType;
2831
}
2932

3033
public object GetValue(object? obj) {

Inasync.PrimitiveAssert/Internals/Numeric.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static bool IsNumeric(Type type) {
1919
|| type == typeof(float)
2020
|| type == typeof(double)
2121
|| type == typeof(decimal)
22+
|| type == typeof(Numeric)
2223
|| Nullable.GetUnderlyingType(type) is Type underingType && IsNumeric(underingType)
2324
;
2425
}

Inasync.PrimitiveAssert/Internals/TypeExtensions.cs

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.Linq;
55
using System.Reflection;
6+
using Commons;
67

78
namespace Inasync {
89

@@ -58,42 +59,28 @@ public static bool IsSystemCollection(this Type type) {
5859
/// <summary>
5960
/// インスタンスかつパブリックな <see cref="DataMember"/> の一覧を返します。
6061
/// </summary>
62+
/// <remarks>
63+
/// 多重継承したインターフェース等が指定された場合に、同じ名前のデータメンバーが列挙される可能性があります。
64+
/// これを一意に識別する場合は、宣言された型 (DeclaringType) も併せて識別子として下さい。
65+
/// </remarks>
6166
public static IEnumerable<DataMember> GetDataMembers(this Type type) {
67+
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
68+
6269
var props = (
63-
from prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
70+
from prop in type.GetPropertiesEx(bindingFlags)
6471
where prop.GetIndexParameters().Length == 0
6572
where prop.GetGetMethod() != null
6673
select new DataMember(prop)
6774
);
6875

6976
var fields = (
70-
from field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)
77+
from field in type.GetFields(bindingFlags)
7178
select new DataMember(field)
7279
);
7380

7481
return props.Concat(fields);
7582
}
7683

77-
/// <summary>
78-
/// 指定した名前の、インスタンスかつパブリックな <see cref="DataMember"/> を探します。
79-
/// </summary>
80-
public static bool TryGetDataMember(this Type type, string name, out DataMember result) {
81-
var prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public);
82-
if (prop != null) {
83-
result = new DataMember(prop);
84-
return true;
85-
}
86-
87-
var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public);
88-
if (field != null) {
89-
result = new DataMember(field);
90-
return true;
91-
}
92-
93-
result = default;
94-
return false;
95-
}
96-
9784
/// <summary>
9885
/// <paramref name="duckType"/> のインスタンスかつパブリックな <see cref="DataMember"/> が全て実装されているかどうか。
9986
/// </summary>

Inasync.PrimitiveAssert/PrimitiveAssert.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static class PrimitiveAssert {
2222
/// <param name="expected">比較対象となる期待値。</param>
2323
/// <param name="message">検証に失敗した際に、例外に含まれるメッセージ。</param>
2424
/// <exception cref="PrimitiveAssertFailedException"><paramref name="actual"/> と <paramref name="expected"/> が等価ではありません。</exception>
25+
/// <exception cref="ArgumentException">ターゲット型に同じ名前のデータメンバーが 2 つ以上存在します。</exception>
2526
public static void AssertIs<TTarget>(this TTarget actual, object? expected, string? message = null) {
2627
actual.AssertIs(typeof(TTarget), expected, message);
2728
}
@@ -35,6 +36,7 @@ public static void AssertIs<TTarget>(this TTarget actual, object? expected, stri
3536
/// <param name="expected">比較対象となる期待値。</param>
3637
/// <param name="message">検証に失敗した際に、例外に含まれるメッセージ。</param>
3738
/// <exception cref="PrimitiveAssertFailedException"><paramref name="actual"/> と <paramref name="expected"/> が等価ではありません。</exception>
39+
/// <exception cref="ArgumentException">ターゲット型に同じ名前のデータメンバーが 2 つ以上存在します。</exception>
3840
/// <remarks><paramref name="actual"/> と <paramref name="expected"/> に対して対称式となります。</remarks>
3941
public static void AssertIs<TTarget>(this object? actual, object? expected, string? message = null) {
4042
actual.AssertIs(typeof(TTarget), expected, message);
@@ -49,6 +51,7 @@ public static void AssertIs<TTarget>(this object? actual, object? expected, stri
4951
/// <param name="expected">比較対象となる期待値。</param>
5052
/// <param name="message">検証に失敗した際に、例外に含まれるメッセージ。</param>
5153
/// <exception cref="PrimitiveAssertFailedException"><paramref name="actual"/> と <paramref name="expected"/> が等価ではありません。</exception>
54+
/// <exception cref="ArgumentException">ターゲット型に同じ名前のデータメンバーが 2 つ以上存在します。</exception>
5255
/// <remarks><paramref name="actual"/> と <paramref name="expected"/> に対して対称式となります。</remarks>
5356
public static void AssertIs(this object? actual, Type? targetType, object? expected, string? message = null) {
5457
var assert = new AssertIsImpl(message, ConsoleLogging ? _consoleLogger : null);

0 commit comments

Comments
 (0)