Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/web_ui/test/engine/canvas_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import 'package:ui/src/engine.dart';

import 'package:ui/ui.dart' as ui;

import '../matchers.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}
Expand All @@ -31,7 +33,7 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
expect(value.length, equals(16));
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
expect(value[r*4 + c], closeTo(expected[r*4 + c], 1e-10));
expect(value[r*4 + c], moreOrLessEquals(expected[r*4 + c]));
}
}
}
Expand Down
37 changes: 15 additions & 22 deletions lib/web_ui/test/geometry_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import 'package:test/test.dart';

import 'package:ui/ui.dart';

import 'matchers.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}
Expand All @@ -32,33 +34,24 @@ void testMain() {
test('Offset.fromDirection', () {
expect(Offset.fromDirection(0.0, 0.0), const Offset(0.0, 0.0));
expect(Offset.fromDirection(pi / 2.0).dx,
closeTo(0.0, 1e-12)); // aah, floating point math. i love you so.
moreOrLessEquals(0.0)); // aah, floating point math. i love you so.
expect(Offset.fromDirection(pi / 2.0).dy, 1.0);
expect(Offset.fromDirection(-pi / 2.0).dx, closeTo(0.0, 1e-12));
expect(Offset.fromDirection(-pi / 2.0).dx, moreOrLessEquals(0.0));
expect(Offset.fromDirection(-pi / 2.0).dy, -1.0);
expect(Offset.fromDirection(0.0), const Offset(1.0, 0.0));
expect(Offset.fromDirection(pi / 4.0).dx,
closeTo(1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(pi / 4.0).dy,
closeTo(1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(-pi / 4.0).dx,
closeTo(1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(-pi / 4.0).dy,
closeTo(-1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(pi / 4.0).dx, moreOrLessEquals(1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(pi / 4.0).dy, moreOrLessEquals(1.0 / math.sqrt(2.0)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have within that supports types other than numbers. For example, it could collapse the above two lines into one. It doesn't have a built-in epsilon value, but we could teach it. We own the implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

within already takes a distance parameter which would basically be the epsilon value in this case:

expect(
  Offset.fromDirection(pi / 4.0),
  within(
    distance: epsilon,
    from: Offset(1.0 / math.sqrt(2.0), 1.0 / math.sqrt(2.0)),
  ),
);

Maybe we should make moreOrLessEquals generic? Like:

Matcher moreOrLessEquals<T>(
  T value, {
  double epsilon = precisionErrorTolerance,
  DistanceFunction<T>? distanceFunction,
}) {
  ...
}

Then the above two lines become:

expect(
  Offset.fromDirection(pi / 4.0),
  moreOrLessEquals(Offset(1.0 / math.sqrt(2.0), 1.0 / math.sqrt(2.0))),
);

@ditman ditman Feb 1, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make distance in within default to precisionErrorTolerance, then it becomes moreOrLessEquals to moreOrLessEquals, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I would force AnyDistanceFunction to return a double rather than num, but that's just me)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea @ditman !

expect(Offset.fromDirection(-pi / 4.0).dx, moreOrLessEquals(1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(-pi / 4.0).dy, moreOrLessEquals(-1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(pi).dx, -1.0);
expect(Offset.fromDirection(pi).dy, closeTo(0.0, 1e-12));
expect(Offset.fromDirection(pi * 3.0 / 4.0).dx,
closeTo(-1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(pi * 3.0 / 4.0).dy,
closeTo(1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(-pi * 3.0 / 4.0).dx,
closeTo(-1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(-pi * 3.0 / 4.0).dy,
closeTo(-1.0 / math.sqrt(2.0), 1e-12));
expect(Offset.fromDirection(pi).dy, moreOrLessEquals(0.0));
expect(Offset.fromDirection(pi * 3.0 / 4.0).dx, moreOrLessEquals(-1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(pi * 3.0 / 4.0).dy, moreOrLessEquals(1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(-pi * 3.0 / 4.0).dx, moreOrLessEquals(-1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(-pi * 3.0 / 4.0).dy, moreOrLessEquals(-1.0 / math.sqrt(2.0)));
expect(Offset.fromDirection(0.0, 2.0), const Offset(2.0, 0.0));
expect(
Offset.fromDirection(pi / 6, 2.0).dx, closeTo(math.sqrt(3.0), 1e-12));
expect(Offset.fromDirection(pi / 6, 2.0).dy, closeTo(1.0, 1e-12));
expect(Offset.fromDirection(pi / 6, 2.0).dx, moreOrLessEquals(math.sqrt(3.0)));
expect(Offset.fromDirection(pi / 6, 2.0).dy, moreOrLessEquals(1.0));
});
test('Size.aspectRatio', () {
expect(const Size(0.0, 0.0).aspectRatio, 0.0);
Expand Down
43 changes: 19 additions & 24 deletions lib/web_ui/test/lerp_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ import 'package:test/test.dart';

import 'package:ui/ui.dart';

/// The epsilon of tolerable double precision error.
///
/// This is used in various places in the framework to allow for floating point
/// precision loss in calculations. Differences below this threshold are safe
/// to disregard.
const double precisionErrorTolerance = 1e-10;
import 'matchers.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
Expand All @@ -31,19 +26,19 @@ void testMain() {
});

test('lerpDouble should treat a null input as 0 if the other input is non-null', () {
expect(lerpDouble(null, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance));
expect(lerpDouble(10.0, null, 0.25), closeTo(7.5, precisionErrorTolerance));
expect(lerpDouble(null, 10.0, 0.25), moreOrLessEquals(2.5));
expect(lerpDouble(10.0, null, 0.25), moreOrLessEquals(7.5));

expect(lerpDouble(null, 10, 0.25), closeTo(2.5, precisionErrorTolerance));
expect(lerpDouble(10, null, 0.25), closeTo(7.5, precisionErrorTolerance));
expect(lerpDouble(null, 10, 0.25), moreOrLessEquals(2.5));
expect(lerpDouble(10, null, 0.25), moreOrLessEquals(7.5));
});

test('lerpDouble should handle interpolation values < 0.0', () {
expect(lerpDouble(0.0, 10.0, -5.0), closeTo(-50.0, precisionErrorTolerance));
expect(lerpDouble(10.0, 0.0, -5.0), closeTo(60.0, precisionErrorTolerance));
expect(lerpDouble(0.0, 10.0, -5.0), moreOrLessEquals(-50.0));
expect(lerpDouble(10.0, 0.0, -5.0), moreOrLessEquals(60.0));

expect(lerpDouble(0, 10, -5), closeTo(-50, precisionErrorTolerance));
expect(lerpDouble(10, 0, -5), closeTo(60, precisionErrorTolerance));
expect(lerpDouble(0, 10, -5), moreOrLessEquals(-50));
expect(lerpDouble(10, 0, -5), moreOrLessEquals(60));
});

test('lerpDouble should return the start value at 0.0', () {
Expand All @@ -55,17 +50,17 @@ void testMain() {
});

test('lerpDouble should interpolate between two values', () {
expect(lerpDouble(0.0, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance));
expect(lerpDouble(10.0, 0.0, 0.25), closeTo(7.5, precisionErrorTolerance));
expect(lerpDouble(0.0, 10.0, 0.25), moreOrLessEquals(2.5));
expect(lerpDouble(10.0, 0.0, 0.25), moreOrLessEquals(7.5));

expect(lerpDouble(0, 10, 0.25), closeTo(2.5, precisionErrorTolerance));
expect(lerpDouble(10, 0, 0.25), closeTo(7.5, precisionErrorTolerance));
expect(lerpDouble(0, 10, 0.25), moreOrLessEquals(2.5));
expect(lerpDouble(10, 0, 0.25), moreOrLessEquals(7.5));

// Exact answer: 20.0 - 1.0e-29
expect(lerpDouble(10.0, 1.0e30, 1.0e-29), closeTo(20.0, precisionErrorTolerance));
expect(lerpDouble(10.0, 1.0e30, 1.0e-29), moreOrLessEquals(20.0));

// Exact answer: 5.0 + 5.0e29
expect(lerpDouble(10.0, 1.0e30, 0.5), closeTo(5.0e29, precisionErrorTolerance));
expect(lerpDouble(10.0, 1.0e30, 0.5), moreOrLessEquals(5.0e29));
});

test('lerpDouble should return the end value at 1.0', () {
Expand All @@ -80,11 +75,11 @@ void testMain() {
});

test('lerpDouble should handle interpolation values > 1.0', () {
expect(lerpDouble(0.0, 10.0, 5.0), closeTo(50.0, precisionErrorTolerance));
expect(lerpDouble(10.0, 0.0, 5.0), closeTo(-40.0, precisionErrorTolerance));
expect(lerpDouble(0.0, 10.0, 5.0), moreOrLessEquals(50.0));
expect(lerpDouble(10.0, 0.0, 5.0), moreOrLessEquals(-40.0));

expect(lerpDouble(0, 10, 5), closeTo(50, precisionErrorTolerance));
expect(lerpDouble(10, 0, 5), closeTo(-40, precisionErrorTolerance));
expect(lerpDouble(0, 10, 5), moreOrLessEquals(50));
expect(lerpDouble(10, 0, 5), moreOrLessEquals(-40));
});

test('lerpDouble should return input value in all cases if begin/end are equal', () {
Expand Down
58 changes: 58 additions & 0 deletions lib/web_ui/test/matchers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';

/// The epsilon of tolerable double precision error.
///
/// This is used in various places in the framework to allow for floating point
/// precision loss in calculations. Differences below this threshold are safe
/// to disregard.
const double precisionErrorTolerance = 1e-10;

/// Enumerates all persisted surfaces in the tree rooted at [root].
///
/// If [root] is `null` returns all surfaces from the last rendered scene.
Expand Down Expand Up @@ -496,3 +503,54 @@ final Matcher throwsAssertionError = throwsA(isAssertionError);
/// * [throwsAssertionError], to test if a function throws any [AssertionError].
/// * [isFlutterError], to test if any object is a [FlutterError].
const Matcher isAssertionError = TypeMatcher<AssertionError>();

/// Asserts that two [double]s are equal, within some tolerated error.
///
/// {@template flutter.flutter_test.moreOrLessEquals}
/// Two values are considered equal if the difference between them is within
/// [precisionErrorTolerance] of the larger one. This is an arbitrary value
/// which can be adjusted using the `epsilon` argument. This matcher is intended
/// to compare floating point numbers that are the result of different sequences
/// of operations, such that they may have accumulated slightly different
/// errors.
/// {@endtemplate}
///
/// See also:
///
/// * [closeTo], which is identical except that the epsilon argument is
/// required and not named.
/// * [inInclusiveRange], which matches if the argument is in a specified
/// range.
/// * [rectMoreOrLessEquals] and [offsetMoreOrLessEquals], which do something
/// similar but for [Rect]s and [Offset]s respectively.
Matcher moreOrLessEquals(double value, { double epsilon = precisionErrorTolerance }) {
Comment thread
ditman marked this conversation as resolved.
Outdated
return _MoreOrLessEquals(value, epsilon);
}

class _MoreOrLessEquals extends Matcher {
const _MoreOrLessEquals(this.value, this.epsilon)
: assert(epsilon >= 0);

final double value;
final double epsilon;

@override
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
if (object is! double) {
return false;
}
if (object == value) {
return true;
}
return (object - value).abs() <= epsilon;
}

@override
Description describe(Description description) => description.add('$value (±$epsilon)');

@override
Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
return super.describeMismatch(item, mismatchDescription, matchState, verbose)
..add('$item is not in the range of $value (±$epsilon).');
}
}