Skip to content

Commit

Permalink
feat: Implement closeToVector4 and closeToQuaternion by extracing a g…
Browse files Browse the repository at this point in the history
…eneric CloseToVector base (#3480)

This creates a generic `CloseToVector` class to be used to power the
existing `closeToVector2` and `closeToVector3` and brand-new
`closeToVector4` and `closeToQuaternion` matchers. This base class is
not exposed (for now; but we could reconsider if demand arises).
  • Loading branch information
luanpotter authored Feb 9, 2025
1 parent 8db2476 commit 57e5851
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 53 deletions.
2 changes: 2 additions & 0 deletions packages/flame_test/lib/flame_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export 'src/close_to_aabb.dart';
export 'src/close_to_matrix4.dart';
export 'src/close_to_quaternion.dart';
export 'src/close_to_vector.dart';
export 'src/close_to_vector3.dart';
export 'src/close_to_vector4.dart';
export 'src/debug_text_renderer.dart';
export 'src/expect_color.dart';
export 'src/expect_double.dart';
Expand Down
26 changes: 26 additions & 0 deletions packages/flame_test/lib/src/close_to_quaternion.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flame_test/src/is_close_to_vector.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math.dart';

/// Returns a matcher which checks if the argument is a Quaternion within
/// distance [epsilon] of [quaternion]. For example:
///
/// ```dart
/// expect(
/// rotation,
/// closeToQuaternion(Quaternion.axisAngle(Vector3(1, 0, 0), pi / 2)),
/// );
/// ```
Matcher closeToQuaternion(Quaternion quaternion, [double epsilon = 1e-15]) {
return _IsCloseToQuaternion(quaternion, epsilon);
}

class _IsCloseToQuaternion extends IsCloseToVector<Quaternion> {
const _IsCloseToQuaternion(super.value, super.epsilon);

@override
double dist(Quaternion a, Quaternion b) => (a - b).length;

@override
List<double> storage(Quaternion value) => value.storage;
}
31 changes: 6 additions & 25 deletions packages/flame_test/lib/src/close_to_vector.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flame_test/src/is_close_to_vector.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart';

Expand All @@ -9,35 +10,15 @@ import 'package:vector_math/vector_math_64.dart';
/// expect(position, closeToVector(expectedPosition, 1e-10));
/// ```
Matcher closeToVector(Vector2 vector, [double epsilon = 1e-15]) {
return _IsCloseTo(vector, epsilon);
return _IsCloseToVector2(vector, epsilon);
}

class _IsCloseTo extends Matcher {
const _IsCloseTo(this._value, this._epsilon);

final Vector2 _value;
final double _epsilon;

@override
bool matches(dynamic item, Map matchState) {
return (item is Vector2) && (item - _value).length <= _epsilon;
}
class _IsCloseToVector2 extends IsCloseToVector<Vector2> {
const _IsCloseToVector2(super.value, super.epsilon);

@override
Description describe(Description description) => description
.add('a Vector2 object within $_epsilon of (${_value.x}, ${_value.y})');
double dist(Vector2 a, Vector2 b) => (a - b).length;

@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map matchState,
bool verbose,
) {
if (item is! Vector2) {
return mismatchDescription.add('is not an instance of Vector2');
}
final distance = (item - _value).length;
return mismatchDescription.add('is at distance $distance');
}
List<double> storage(Vector2 value) => value.storage;
}
33 changes: 6 additions & 27 deletions packages/flame_test/lib/src/close_to_vector3.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flame_test/src/is_close_to_vector.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math.dart';

Expand All @@ -9,37 +10,15 @@ import 'package:vector_math/vector_math.dart';
/// expect(position, closeToVector3(expectedPosition, 1e-10));
/// ```
Matcher closeToVector3(Vector3 vector, [double epsilon = 1e-15]) {
return _IsCloseTo(vector, epsilon);
return _IsCloseToVector3(vector, epsilon);
}

class _IsCloseTo extends Matcher {
const _IsCloseTo(this._value, this._epsilon);

final Vector3 _value;
final double _epsilon;

@override
bool matches(dynamic item, Map matchState) {
return (item is Vector3) && (item - _value).length <= _epsilon;
}
class _IsCloseToVector3 extends IsCloseToVector<Vector3> {
const _IsCloseToVector3(super.value, super.epsilon);

@override
Description describe(Description description) {
final coords = '${_value.x}, ${_value.y}, ${_value.z}';
return description.add('a Vector3 object within $_epsilon of ($coords)');
}
double dist(Vector3 a, Vector3 b) => (a - b).length;

@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map matchState,
bool verbose,
) {
if (item is! Vector3) {
return mismatchDescription.add('is not an instance of Vector3');
}
final distance = (item - _value).length;
return mismatchDescription.add('is at distance $distance');
}
List<double> storage(Vector3 value) => value.storage;
}
24 changes: 24 additions & 0 deletions packages/flame_test/lib/src/close_to_vector4.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flame_test/src/is_close_to_vector.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math.dart';

/// Returns a matcher which checks if the argument is a 4d vector within
/// distance [epsilon] of [vector]. For example:
///
/// ```dart
/// expect(matrix4.row1, closeToVector4(Vector4(1, 0, 0, 1)));
/// expect(matrix4.row2, closeToVector4(Vector4(0, 1, 0, -1), 1e-10));
/// ```
Matcher closeToVector4(Vector4 vector, [double epsilon = 1e-15]) {
return _IsCloseToVector4(vector, epsilon);
}

class _IsCloseToVector4 extends IsCloseToVector<Vector4> {
const _IsCloseToVector4(super.value, super.epsilon);

@override
double dist(Vector4 a, Vector4 b) => (a - b).length;

@override
List<double> storage(Vector4 value) => value.storage;
}
36 changes: 36 additions & 0 deletions packages/flame_test/lib/src/is_close_to_vector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter_test/flutter_test.dart';

abstract class IsCloseToVector<V> extends Matcher {
const IsCloseToVector(this._value, this._epsilon);

final V _value;
final double _epsilon;

double dist(V a, V b);
List<double> storage(V value);

@override
bool matches(dynamic item, Map matchState) {
return (item is V) && dist(item, _value) <= _epsilon;
}

@override
Description describe(Description description) {
final coords = storage(_value).join(', ');
return description.add('a $V object within $_epsilon of ($coords)');
}

@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map matchState,
bool verbose,
) {
if (item is! V) {
return mismatchDescription.add('is not an instance of $V');
}
final distance = dist(item, _value);
return mismatchDescription.add('is at distance $distance');
}
}
69 changes: 69 additions & 0 deletions packages/flame_test/test/close_to_quaternion_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math.dart';

void main() {
group('closeToQuaternion', () {
test('matches normally', () {
expect(
Quaternion.fromRotation(Matrix3.identity()),
closeToQuaternion(Quaternion(0, 0, 0, 1)),
);
expect(
Quaternion(-14, 99, -99, 14),
closeToQuaternion(Quaternion(-14, 99, -99, 14)),
);
expect(
Quaternion(1e-20, -1e-16, 0, -0),
closeToQuaternion(Quaternion(0, 0, 0, 0)),
);

expect(
Quaternion(1.0001, 2.0, -1.0001, -0),
closeToQuaternion(Quaternion(1, 2, -1, 0), 0.01),
);
expect(
Quaternion(5, 9, 11, 15),
closeToQuaternion(Quaternion(10, 10, 10, 10), 10),
);
});

test('fails on type mismatch', () {
try {
expect(4.14, closeToQuaternion(Quaternion(0, 0, 0, 0)));
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Quaternion object within 1e-15 of '
'(0.0, 0.0, 0.0, 0.0)',
),
);
expect(e.message, contains('Actual: <4.14>'));
expect(e.message, contains('Which: is not an instance of Quaternion'));
}
});

test('fails on value mismatch', () {
try {
expect(
Quaternion(101, 217, 100, 0),
closeToQuaternion(Quaternion(100, 220, 101, 0)),
);
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Quaternion object within 1e-15 of '
'(100.0, 220.0, 101.0, 0.0)',
),
);
expect(
e.message,
contains('Actual: Quaternion:<101.0, 217.0, 100.0 @ 0.0>'),
);
expect(e.message, contains('Which: is at distance 3.3166247903554'));
}
});
});
}
15 changes: 15 additions & 0 deletions packages/flame_test/test/close_to_vector3_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ void main() {
}
});

test('fails on type mismatch - vector4', () {
try {
expect(Vector4(1, 2, 3, 4), closeToVector3(Vector3.zero()));
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Vector3 object within 1e-15 of (0.0, 0.0, 0.0)',
),
);
expect(e.message, contains('Actual: Vector4:<1.0,2.0,3.0,4.0>'));
expect(e.message, contains('Which: is not an instance of Vector3'));
}
});

test('fails on value mismatch', () {
try {
expect(Vector3(101, 217, 100), closeToVector3(Vector3(100, 220, 101)));
Expand Down
92 changes: 92 additions & 0 deletions packages/flame_test/test/close_to_vector4_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:flame_test/src/close_to_vector4.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math.dart';

void main() {
group('closeToVector4', () {
test('matches normally', () {
expect(Vector4.zero(), closeToVector4(Vector4(0, 0, 0, 0)));
expect(
Vector4(-14, 99, -99, 14),
closeToVector4(Vector4(-14, 99, -99, 14)),
);
expect(
Vector4(1e-20, -1e-16, 0, -0),
closeToVector4(Vector4(0, 0, 0, 0)),
);

expect(
Vector4(1.0001, 2.0, -1.0001, -0),
closeToVector4(Vector4(1, 2, -1, 0), 0.01),
);
expect(Vector4(5, 9, 11, 15), closeToVector4(Vector4.all(10), 10));
});

test('fails on type mismatch - double', () {
try {
expect(4.14, closeToVector4(Vector4.zero()));
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Vector4 object within 1e-15 of (0.0, 0.0, 0.0, 0.0)',
),
);
expect(e.message, contains('Actual: <4.14>'));
expect(e.message, contains('Which: is not an instance of Vector4'));
}
});

test('fails on type mismatch - vector2', () {
try {
expect(Vector2(1, 2), closeToVector4(Vector4.zero()));
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Vector4 object within 1e-15 of (0.0, 0.0, 0.0, 0.0)',
),
);
expect(e.message, contains('Actual: Vector2:<[1.0,2.0]>'));
expect(e.message, contains('Which: is not an instance of Vector4'));
}
});

test('fails on type mismatch - vector3', () {
try {
expect(Vector3(1, 2, 3), closeToVector4(Vector4.zero()));
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Vector4 object within 1e-15 of (0.0, 0.0, 0.0, 0.0)',
),
);
expect(e.message, contains('Actual: Vector3:<[1.0,2.0,3.0]>'));
expect(e.message, contains('Which: is not an instance of Vector4'));
}
});

test('fails on value mismatch', () {
try {
expect(
Vector4(101, 217, 100, 0),
closeToVector4(Vector4(100, 220, 101, 0)),
);
} on TestFailure catch (e) {
expect(
e.message,
contains(
'Expected: a Vector4 object within 1e-15 of '
'(100.0, 220.0, 101.0, 0.0)',
),
);
expect(
e.message,
contains('Actual: Vector4:<101.0,217.0,100.0,0.0>'),
);
expect(e.message, contains('Which: is at distance 3.3166247903554'));
}
});
});
}
Loading

0 comments on commit 57e5851

Please sign in to comment.