Skip to content

Commit a6f77e6

Browse files
authored
feat(tests): Extend the compilation unit class matchers. (serverpod#3355)
1 parent 631baab commit a6f77e6

17 files changed

+1001
-193
lines changed

tools/serverpod_cli/test/test_util/compilation_unit_matcher.dart

+30-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import 'compilation_unit_matcher/chainable_matcher.dart';
88
part 'compilation_unit_matcher/argument_matcher/argument_matcher.dart';
99
part 'compilation_unit_matcher/class_matcher/class_matcher.dart';
1010
part 'compilation_unit_matcher/constructor_matcher/constructor_matcher.dart';
11+
part 'compilation_unit_matcher/extends_matcher/extends_matcher.dart';
1112
part 'compilation_unit_matcher/field_matcher/field_matcher.dart';
13+
part 'compilation_unit_matcher/generic_matcher/generic_matcher.dart';
1214
part 'compilation_unit_matcher/method_matcher/method_matcher.dart';
1315
part 'compilation_unit_matcher/parameter_matcher/parameter_matcher.dart';
1416
part 'compilation_unit_matcher/super_initializer_matcher/super_initializer_matcher.dart';
@@ -28,8 +30,10 @@ part 'compilation_unit_matcher/super_initializer_matcher/super_initializer_match
2830
/// │ ├── SuperInitializerMatcher
2931
/// │ │ └── ArgumentMatcher
3032
/// │ └── ParameterMatcher
31-
/// └── MethodMatcher
32-
/// └── ParameterMatcher
33+
/// ├── MethodMatcher
34+
/// │ └── ParameterMatcher
35+
/// └── ExtendsMatcher
36+
///
3337
ClassMatcher containsClass(String className) => _ClassMatcherImpl._(className);
3438

3539
/// Parses a string of Dart code into a FormattedCompilationUnit.
@@ -42,6 +46,9 @@ abstract interface class ArgumentMatcher {}
4246
/// A matcher that checks if a CompilationUnit contains a class that matches
4347
/// certain criteria.
4448
abstract interface class ClassMatcher {
49+
/// Chains a [ExtendsMatcher] that checks if the class extends a specific class.
50+
ExtendsMatcher thatExtends(String className);
51+
4552
/// Chains a [FieldMatcher] that checks if the class contains a field with a
4653
/// specific name.
4754
///
@@ -56,7 +63,18 @@ abstract interface class ClassMatcher {
5663
///
5764
/// Use [isNullable] to match field nullability. If the value is not set, the
5865
/// matcher will ignore the nullability of the field.
59-
FieldMatcher withField(String fieldName, {bool? isNullable});
66+
///
67+
/// Use [isFinal] to match final fields. If the value is not set, the matcher
68+
/// will ignore the final status of the field
69+
///
70+
/// Use [isLate] to match late fields. If the value is not set, the matcher
71+
/// will ignore the late status of the field
72+
FieldMatcher withField(
73+
String fieldName, {
74+
bool? isNullable,
75+
bool? isFinal,
76+
bool? isLate,
77+
});
6078

6179
/// Chains a [MethodMatcher] that checks if the class contains a method with a
6280
/// specific name.
@@ -139,6 +157,11 @@ abstract interface class ConstructorMatcher {
139157
});
140158
}
141159

160+
/// A chainable matcher that matches the extension in a compilation unit.
161+
abstract interface class ExtendsMatcher {
162+
GenericMatcher withGeneric(String genericType);
163+
}
164+
142165
/// A chainable matcher that matches a field in a compilation unit.
143166
abstract interface class FieldMatcher {}
144167

@@ -153,6 +176,9 @@ class FormattedCompilationUnit {
153176
}
154177
}
155178

179+
/// A chainable matcher that matches a generic type in a compilation unit.
180+
abstract interface class GenericMatcher {}
181+
156182
/// Initializer types for parameters.
157183
enum Initializer {
158184
/// The parameter is initialized with `this`.
@@ -196,7 +222,7 @@ abstract interface class ParameterMatcher {}
196222
/// A chainable matcher that matches a super initializer in a compilation unit.
197223
abstract interface class SuperInitializerMatcher {
198224
/// Chains an [ArgumentMatcher] that checks if the super initializer is called
199-
/// with a specific argument.
225+
/// with a specific literal argument.
200226
ArgumentMatcher withArgument(String value);
201227

202228
/// Chains an [ArgumentMatcher] that checks if the super initializer is called

tools/serverpod_cli/test/test_util/compilation_unit_matcher/argument_matcher/argument_matcher.dart

+5-7
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class _ArgumentMatcherImpl extends Matcher implements ArgumentMatcher {
4747
'does not contain argument "$_value" in super initializer. Found arguments: [',
4848
);
4949
output.writeAll(
50-
arguments.map((e) => e.toSource()),
50+
arguments.value.map((e) => e.toSource()),
5151
', ',
5252
);
5353
output.write(']');
@@ -89,10 +89,10 @@ class _ArgumentMatcherImpl extends Matcher implements ArgumentMatcher {
8989
}
9090

9191
Iterable<Expression>? _featureValueOf(actual) {
92-
var superInitializer = _parent.matchedFeatureValueOf(actual);
93-
if (superInitializer == null) return null;
92+
var match = _parent.matchedFeatureValueOf(actual);
93+
if (match == null) return null;
9494

95-
return superInitializer.where((e) => e._hasMatchingValue(_value));
95+
return match.value.where((e) => e._hasMatchingValue(_value));
9696
}
9797
}
9898

@@ -134,8 +134,6 @@ extension on Expression {
134134
return resolvedThis.expression._hasMatchingValue(name);
135135
}
136136

137-
if (resolvedThis is! SimpleIdentifier) return false;
138-
139-
return resolvedThis.name == name;
137+
return resolvedThis.toSource() == name;
140138
}
141139
}

tools/serverpod_cli/test/test_util/compilation_unit_matcher/argument_matcher/argument_matcher_test.dart

+47
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,51 @@ void main() {
191191
);
192192
});
193193
});
194+
195+
group(
196+
'Given compilation unit with class with super initializer with literal argument',
197+
() {
198+
late final compilationUnit = parseCode(
199+
'''
200+
class User extends Super {
201+
User() : super('name');
202+
}
203+
''',
204+
);
205+
206+
test(
207+
'when matching class, constructor, super initializer and argument then test passes',
208+
() {
209+
expect(
210+
compilationUnit,
211+
containsClass('User')
212+
.withUnnamedConstructor()
213+
.withSuperInitializer()
214+
.withArgument("'name'"),
215+
);
216+
});
217+
});
218+
219+
group(
220+
'Given compilation unit with class with super initializer with identifier argument',
221+
() {
222+
late final compilationUnit = parseCode(
223+
'''
224+
class User extends Super {
225+
User() : super(name);
226+
}
227+
''',
228+
);
229+
test(
230+
'when matching class, constructor, super initializer and argument then test passes',
231+
() {
232+
expect(
233+
compilationUnit,
234+
containsClass('User')
235+
.withUnnamedConstructor()
236+
.withSuperInitializer()
237+
.withArgument('name'),
238+
);
239+
});
240+
});
194241
}

tools/serverpod_cli/test/test_util/compilation_unit_matcher/chainable_matcher.dart

+53-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
import 'package:test/test.dart';
22

3-
typedef MatchedFeatureValueOf<T> = T? Function(dynamic actual);
3+
/// Helper method to construct a [Match] object.
4+
///
5+
/// In order to construct a [Match] object, the [matchedItem] must be non-null.
6+
/// If the [matchedItem] is null, this method will return null.
7+
///
8+
/// [extractValue] is used to extract the value from the [matchedItem]
9+
/// that is then stored in the constructed [Match] object.
10+
Match<T>? _createMatch<T, V>({
11+
required V? Function() resolveMatch,
12+
required T Function(V value) extractValue,
13+
}) {
14+
var matchedItem = resolveMatch();
15+
if (matchedItem == null) return null;
16+
17+
return Match(extractValue(matchedItem));
18+
}
19+
20+
typedef MatchedFeatureValueOf<T> = Match<T>? Function(dynamic actual);
421

522
/// A matcher that can be chained with other matchers.
623
class ChainableMatcher<T> {
724
final Matcher _matcher;
825
final MatchedFeatureValueOf<T> _matchedFeatureValueOf;
926

10-
ChainableMatcher(this._matcher, this._matchedFeatureValueOf);
27+
ChainableMatcher._(this._matcher, this._matchedFeatureValueOf);
28+
29+
/// Creates a matcher that can be chained with other matchers.
30+
/// The [matcher] is the matcher that is chained.
31+
///
32+
/// The [resolveMatch] function is used to determine if the contained matcher
33+
/// matches the [actual] value. If the matcher matches, this should return the
34+
/// resolved match. If the matcher does not match, this should return null.
35+
///
36+
/// The [extractValue] function is used to extract the value from the resolved
37+
/// match. This value is then stored in the [Match] object.
38+
static ChainableMatcher<T> createMatcher<T, V>(
39+
Matcher matcher, {
40+
required V? Function(dynamic item) resolveMatch,
41+
required T Function(V value) extractValue,
42+
}) {
43+
return ChainableMatcher<T>._(
44+
matcher,
45+
(actual) => _createMatch<T, V>(
46+
resolveMatch: () => resolveMatch(actual),
47+
extractValue: extractValue,
48+
),
49+
);
50+
}
1151

1252
/// Describes the contained matcher. This is used to describe the matcher in
1353
/// the context of the chain.
@@ -31,7 +71,15 @@ class ChainableMatcher<T> {
3171

3272
/// Checks if the matcher matches the given [actual] value.
3373
///
34-
/// If null is returned, this means that the contained matcher did not match
35-
/// and the call to describeMismatch should be delegated to the matcher.
36-
T? matchedFeatureValueOf(dynamic actual) => _matchedFeatureValueOf(actual);
74+
/// Returns a [Match] object if the matcher matches the [actual] value.
75+
/// If the matcher does not match, this method should return null and the call
76+
/// to describeMismatch should be delegated to the matcher.
77+
Match<T>? matchedFeatureValueOf(dynamic actual) =>
78+
_matchedFeatureValueOf(actual);
79+
}
80+
81+
/// A match that contains the value requested by the matcher.
82+
class Match<T> {
83+
final T value;
84+
Match(this.value);
3785
}

tools/serverpod_cli/test/test_util/compilation_unit_matcher/class_matcher/class_matcher.dart

+38-18
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
part of '../../compilation_unit_matcher.dart';
22

33
class _ClassMatcherImpl implements Matcher, ClassMatcher {
4-
final String className;
5-
_ClassMatcherImpl._(this.className);
4+
final String _className;
5+
6+
_ClassMatcherImpl._(this._className);
67

78
@override
89
Description describe(Description description) {
9-
return description.add('a CompilationUnit containing class "$className"');
10+
return description.add('a CompilationUnit containing class "$_className"');
1011
}
1112

1213
@override
@@ -32,7 +33,7 @@ class _ClassMatcherImpl implements Matcher, ClassMatcher {
3233
.join(', ');
3334

3435
return mismatchDescription.add(
35-
'does not contain class "$className". Found classes: [$classNames]');
36+
'does not contain class "$_className". Found classes: [$classNames]');
3637
}
3738

3839
@override
@@ -41,16 +42,35 @@ class _ClassMatcherImpl implements Matcher, ClassMatcher {
4142
}
4243

4344
@override
44-
FieldMatcher withField(String fieldName, {bool? isNullable}) {
45+
ExtendsMatcher thatExtends(String className) {
46+
return _ExtendsMatcherImpl._(
47+
ChainableMatcher.createMatcher(
48+
this,
49+
resolveMatch: _matchedFeatureValueOf,
50+
extractValue: (classDeclaration) => classDeclaration.extendsClause,
51+
),
52+
name: className,
53+
);
54+
}
55+
56+
@override
57+
FieldMatcher withField(
58+
String fieldName, {
59+
bool? isNullable,
60+
bool? isFinal,
61+
bool? isLate,
62+
}) {
4563
return _FieldMatcherImpl._(
46-
ChainableMatcher(
64+
ChainableMatcher.createMatcher(
4765
this,
48-
(actual) => _matchedFeatureValueOf(actual)
49-
?.members
50-
.whereType<FieldDeclaration>(),
66+
resolveMatch: _matchedFeatureValueOf,
67+
extractValue: (classDeclaration) =>
68+
classDeclaration.members.whereType<FieldDeclaration>(),
5169
),
5270
fieldName,
5371
isNullable: isNullable,
72+
isFinal: isFinal,
73+
isLate: isLate,
5474
);
5575
}
5676

@@ -61,11 +81,11 @@ class _ClassMatcherImpl implements Matcher, ClassMatcher {
6181
String? returnType,
6282
}) {
6383
return _MethodMatcherImpl._(
64-
ChainableMatcher(
84+
ChainableMatcher.createMatcher(
6585
this,
66-
(actual) => _matchedFeatureValueOf(actual)
67-
?.members
68-
.whereType<MethodDeclaration>(),
86+
resolveMatch: _matchedFeatureValueOf,
87+
extractValue: (classDeclaration) =>
88+
classDeclaration.members.whereType<MethodDeclaration>(),
6989
),
7090
methodName,
7191
isOverride: isOverride,
@@ -102,7 +122,7 @@ class _ClassMatcherImpl implements Matcher, ClassMatcher {
102122

103123
return resolvedActual.declarations
104124
.whereType<ClassDeclaration>()
105-
.where((d) => d._hasMatchingClass(className))
125+
.where((d) => d._hasMatchingClass(_className))
106126
.firstOrNull;
107127
}
108128

@@ -124,11 +144,11 @@ class _ClassMatcherImpl implements Matcher, ClassMatcher {
124144
bool? isFactory,
125145
}) {
126146
return _ConstructorMatcherImpl._(
127-
ChainableMatcher(
147+
ChainableMatcher.createMatcher(
128148
this,
129-
(actual) => _matchedFeatureValueOf(actual)
130-
?.members
131-
.whereType<ConstructorDeclaration>(),
149+
resolveMatch: _matchedFeatureValueOf,
150+
extractValue: (classDeclaration) =>
151+
classDeclaration.members.whereType<ConstructorDeclaration>(),
132152
),
133153
name: constructorName,
134154
isFactory: isFactory,

0 commit comments

Comments
 (0)