From 950b09620e9f2413591b8447f1280287618fe045 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 28 Aug 2020 22:37:20 -0700 Subject: [PATCH 1/2] Extract Dart test utilities library This extracts a Dart test utilities library, containing `expectAssertion` and `expectArgumentError` functions that simplify running tests that test assertions across debug, profile, and release configurations. This change also restricts Dart unit tests to testing files whose filename matches `*_test.dart` under `flutter/testing/dart`; previously any file in that directory was run, but all files matched the above pattern. --- testing/dart/canvas_test.dart | 31 ++---------------------- testing/dart/test_util.dart | 44 +++++++++++++++++++++++++++++++++++ testing/run_tests.py | 2 +- 3 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 testing/dart/test_util.dart diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 8640cc86bf63d..39a21a540f678 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -12,6 +12,8 @@ import 'package:image/image.dart' as dart_image; import 'package:path/path.dart' as path; import 'package:test/test.dart'; +import 'test_util.dart'; + typedef CanvasCallback = void Function(Canvas canvas); Future createImage(int width, int height) { @@ -38,35 +40,6 @@ void testCanvas(CanvasCallback callback) { } catch (error) { } // ignore: empty_catches } -void expectAssertion(Function callback) { - bool assertsEnabled = false; - assert(() { - assertsEnabled = true; - return true; - }()); - if (assertsEnabled) { - bool threw = false; - try { - callback(); - } catch (e) { - expect(e is AssertionError, true); - threw = true; - } - expect(threw, true); - } -} - -void expectArgumentError(Function callback) { - bool threw = false; - try { - callback(); - } catch (e) { - expect(e is ArgumentError, true); - threw = true; - } - expect(threw, true); -} - void testNoCrashes() { test('canvas APIs should not crash', () async { final Paint paint = Paint(); diff --git a/testing/dart/test_util.dart b/testing/dart/test_util.dart new file mode 100644 index 0000000000000..e990b138e7f3e --- /dev/null +++ b/testing/dart/test_util.dart @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 + +import 'package:test/test.dart'; + +/// Asserts that `callback` throws an [AssertionError]. +/// +/// When running in a VM in which assertions are enabled, asserts that the +/// specified callback throws an [AssertionError]. When asserts are not +/// enabled, such as when running using a release-mode VM with default +/// settings, this acts as a no-op. +void expectAssertion(Function callback) { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + if (assertsEnabled) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is AssertionError, true); + threw = true; + } + expect(threw, true); + } +} + +/// Asserts that `callback` throws an [ArgumentError]. +void expectArgumentError(Function callback) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is ArgumentError, true); + threw = true; + } + expect(threw, true); +} + diff --git a/testing/run_tests.py b/testing/run_tests.py index a5c7d833fb279..3330cb985db15 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -395,7 +395,7 @@ def RunDartTests(build_dir, filter, verbose_dart_snapshot): # Now that we have the Sky packages at the hardcoded location, run `pub get`. RunEngineExecutable(build_dir, os.path.join('dart-sdk', 'bin', 'pub'), None, flags=['get'], cwd=dart_tests_dir) - dart_tests = glob.glob('%s/*.dart' % dart_tests_dir) + dart_tests = glob.glob('%s/*_test.dart' % dart_tests_dir) for dart_test_file in dart_tests: if filter is not None and os.path.basename(dart_test_file) not in filter: From 0f5d30d95cad3d6d22558b4c333f3e444c8a3618 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 28 Aug 2020 17:09:42 -0700 Subject: [PATCH 2/2] lerpDouble: stricter handling of NaN and infinity Previously, the behaviour of lerpDouble with respect to NaN and infinity was relatively complex and difficult to reason about. This patch simplifies the behaviour with respect to those conditions and adds documentation and tests. In general, if `a == b` or both values are null, infinite, or NaN, `a` is returned. Otherwise we require `a` and `b` and `t` to be finite or null and the result of the linear interpolation is returned. --- lib/ui/lerp.dart | 16 +++++--- testing/dart/lerp_test.dart | 76 ++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/lib/ui/lerp.dart b/lib/ui/lerp.dart index 91a04c2cbf94a..c4841e2ae37b6 100644 --- a/lib/ui/lerp.dart +++ b/lib/ui/lerp.dart @@ -6,14 +6,20 @@ part of dart.ui; -/// Linearly interpolate between two numbers. -// TODO(cbracken): Consider making a and b non-nullable. -// https://github.com/flutter/flutter/issues/64617 +/// Linearly interpolate between two numbers, `a` and `b`, by an extrapolation +/// factor `t`. +/// +/// When `a` and `b` are equal or both NaN, `a` is returned. Otherwise, if +/// `a`, `b`, and `t` are required to be finite or null, and the result of `a + +/// (b - a) * t` is returned, where nulls are defaulted to 0.0. double? lerpDouble(num? a, num? b, double t) { - if (a == null && b == null) - return null; + if (a == b || (a?.isNaN == true) && (b?.isNaN == true)) + return a?.toDouble(); a ??= 0.0; b ??= 0.0; + assert(a.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(b.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(t.isFinite, 't must be finite when interpolating between values'); return a + (b - a) * t as double; } diff --git a/testing/dart/lerp_test.dart b/testing/dart/lerp_test.dart index 26ad7b89b4031..4395e974747f7 100644 --- a/testing/dart/lerp_test.dart +++ b/testing/dart/lerp_test.dart @@ -7,6 +7,8 @@ import 'dart:ui'; import 'package:test/test.dart'; +import 'test_util.dart'; + void main() { test('lerpDouble should return null if and only if both inputs are null', () { expect(lerpDouble(null, null, 1.0), isNull); @@ -65,47 +67,53 @@ void main() { expect(lerpDouble(10, 0, 5), -40); }); - test('lerpDouble should return NaN if any input is NaN', () { - expect(lerpDouble(0.0, 10.0, double.nan), isNaN); - expect(lerpDouble(0.0, double.infinity, double.nan), isNaN); - expect(lerpDouble(0.0, double.nan, 5.0), isNaN); - expect(lerpDouble(0.0, double.nan, double.infinity), isNaN); - expect(lerpDouble(0.0, double.nan, double.nan), isNaN); - expect(lerpDouble(double.infinity, 10.0, double.nan), isNaN); - expect(lerpDouble(double.infinity, double.infinity, double.nan), isNaN); - expect(lerpDouble(double.infinity, double.nan, 5.0), isNaN); - expect(lerpDouble(double.infinity, double.nan, double.infinity), isNaN); - expect(lerpDouble(double.infinity, double.nan, double.nan), isNaN); - expect(lerpDouble(double.nan, 10.0, 5.0), isNaN); - expect(lerpDouble(double.nan, 10.0, double.infinity), isNaN); - expect(lerpDouble(double.nan, 10.0, double.nan), isNaN); - expect(lerpDouble(double.nan, double.infinity, 5.0), isNaN); - expect(lerpDouble(double.nan, double.infinity, double.infinity), isNaN); - expect(lerpDouble(double.nan, double.infinity, double.nan), isNaN); + test('lerpDouble should return input value in all cases if begin/end are equal', () { + expect(lerpDouble(10.0, 10.0, 5.0), 10.0); + expect(lerpDouble(10.0, 10.0, double.nan), 10.0); + expect(lerpDouble(10.0, 10.0, double.infinity), 10.0); + expect(lerpDouble(10.0, 10.0, -double.infinity), 10.0); + + expect(lerpDouble(10, 10, 5.0), 10.0); + expect(lerpDouble(10, 10, double.nan), 10.0); + expect(lerpDouble(10, 10, double.infinity), 10.0); + expect(lerpDouble(10, 10, -double.infinity), 10.0); + expect(lerpDouble(double.nan, double.nan, 5.0), isNaN); - expect(lerpDouble(double.nan, double.nan, double.infinity), isNaN); expect(lerpDouble(double.nan, double.nan, double.nan), isNaN); + expect(lerpDouble(double.nan, double.nan, double.infinity), isNaN); + expect(lerpDouble(double.nan, double.nan, -double.infinity), isNaN); + + expect(lerpDouble(double.infinity, double.infinity, 5.0), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, double.nan), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, double.infinity), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, -double.infinity), double.infinity); + + expect(lerpDouble(-double.infinity, -double.infinity, 5.0), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, double.nan), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, double.infinity), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, -double.infinity), -double.infinity); }); - test('lerpDouble returns NaN if interpolation results in Infinity - Infinity', () { - expect(lerpDouble(double.infinity, 10.0, 5.0), isNaN); - expect(lerpDouble(double.infinity, 10.0, double.infinity), isNaN); - expect(lerpDouble(-double.infinity, 10.0, 5.0), isNaN); - expect(lerpDouble(-double.infinity, 10.0, double.infinity), isNaN); + test('lerpDouble should throw AssertionError if interpolation value is NaN and a != b', () { + expectAssertion(() => lerpDouble(0.0, 10.0, double.nan)); }); - test('lerpDouble returns +/- infinity if interpolating towards an infinity', () { - expect(lerpDouble(double.infinity, 10.0, -5.0)?.isInfinite, isTrue); - expect(lerpDouble(double.infinity, 10.0, -double.infinity)?.isInfinite, isTrue); - expect(lerpDouble(-double.infinity, 10.0, -5.0)?.isInfinite, isTrue); - expect(lerpDouble(-double.infinity, 10.0, -double.infinity)?.isInfinite, isTrue); - expect(lerpDouble(0.0, double.infinity, 5.0)?.isInfinite, isTrue); - expect(lerpDouble(0.0, double.infinity, -5.0)?.isInfinite, isTrue); - expect(lerpDouble(0.0, 10.0, double.infinity)?.isInfinite, isTrue); - expect(lerpDouble(0.0, double.infinity, double.infinity)?.isInfinite, isTrue); + test('lerpDouble should throw AssertionError if interpolation value is +/- infinity and a != b', () { + expectAssertion(() => lerpDouble(0.0, 10.0, double.infinity)); + expectAssertion(() => lerpDouble(0.0, 10.0, -double.infinity)); }); - test('lerpDouble returns NaN if start/end and interpolation value are infinity', () { - expect(lerpDouble(double.infinity, double.infinity, double.infinity), isNaN); + test('lerpDouble should throw AssertionError if either start or end are NaN', () { + expectAssertion(() => lerpDouble(double.nan, 10.0, 5.0)); + expectAssertion(() => lerpDouble(0.0, double.nan, 5.0)); + }); + + test('lerpDouble should throw AssertionError if either start or end are +/- infinity', () { + expectAssertion(() => lerpDouble(double.infinity, 10.0, 5.0)); + expectAssertion(() => lerpDouble(-double.infinity, 10.0, 5.0)); + expectAssertion(() => lerpDouble(0.0, double.infinity, 5.0)); + expectAssertion(() => lerpDouble(0.0, -double.infinity, 5.0)); }); } + +final Matcher throwsAssertionError = throwsA(const TypeMatcher());