diff --git a/checks_ext/CHANGELOG.md b/checks_ext/CHANGELOG.md new file mode 100644 index 00000000..4558177e --- /dev/null +++ b/checks_ext/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 0.0.1 (Unreleased) + +- Initial development version of `checks_ext` containing extensions for `package:checks`. +- Added extensions for standard library types (`DateTime`, `Duration`, `RegExp`, `Match`, `Iterable`, `String`, `num`, and `List` bytes). diff --git a/checks_ext/LICENSE b/checks_ext/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/checks_ext/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/checks_ext/README.md b/checks_ext/README.md new file mode 100644 index 00000000..dda67224 --- /dev/null +++ b/checks_ext/README.md @@ -0,0 +1,95 @@ +# checks_ext + +Extensions for `package:checks` to make testing easier. + +## Usage + +Add a dev-dependency on `checks_ext` using `dart pub add dev:checks_ext` and +import it in your test files: + +```dart +import 'package:checks_ext/checks_ext.dart'; +``` + +## Highlights + +### 📅 DateTime & Duration + +Test dates and durations with ease. + +```dart +check(DateTime.now()).isAfter(DateTime(2020)); +check(DateTime.now()).isBetween(DateTime(2020), DateTime(2030)); +check(DateTime.now()).isCloseTo(DateTime.now(), within: Duration(seconds: 1)); + +check(Duration(seconds: 5)).isGreaterThan(Duration(seconds: 2)); +check(Duration(seconds: 5)).isCloseTo(Duration(seconds: 6), within: Duration(seconds: 2)); +``` + +### 🔤 RegExp & Match + +Powerful assertions for regular expressions. + +```dart +final regex = RegExp(r'(\d+)'); +check(regex).hasMatch('123'); +check(regex).hasNoMatch('abc'); + +// Check specific matches +check(regex).firstMatch('123').isNotNull()[1].equals('123'); +``` + +### 📜 Iterable Extensions + +Check sorting and handle nulls. + +```dart +check([1, 2, 3]).isSorted(); +check([{'age': 20}, {'age': 30}]).isSortedBy((m) => m['age'] as int); +check([1, 2, 3]).containsAll([1, 2]); + +// Skip nulls in an iterable +check([1, null, 2]).nonNulls.deepEquals([1, 2]); +``` + +### 🔤 String Checks + +Additional checks for strings. + +```dart +check(' ').isBlank(); +check('abc').isLowerCase(); +check('Hello World').containsIgnoringCase('world'); +check('a\nb\nc').lines.deepEquals(['a', 'b', 'c']); +``` + +### 🔢 Numeric Checks + +Checks for `num`, `int`, and `double`. + +```dart +check(5).isBetween(1, 10); +check(2).isEven(); +check(4).isMultipleOf(2); +check(1.0).isFinite(); +``` + +### 🔢 List (Bytes) + +Specialized checks for byte arrays, featuring beautiful hex/ASCII diffs on failure. + +```dart +check([72, 101, 108, 108, 111]).equalsBytes([72, 101, 108, 108, 111]); +check([72, 101, 108, 108, 111]).isValidUtf8(); +``` + +If `equalsBytes` fails, it provides a helpful diff: +``` +Expected: a List that: + equals [72, 101, 108, 108, 111] +Actual: [72, 101, 108, 108, 112] +Which: differs at offset 4: +Actual: H e l l [p] +Expected: H e l l [o] +``` + diff --git a/checks_ext/analysis_options.yaml b/checks_ext/analysis_options.yaml new file mode 100644 index 00000000..f29574ef --- /dev/null +++ b/checks_ext/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + errors: + doc_directive_unknown: ignore # TODO Remove then when @example is supported diff --git a/checks_ext/dart_test.yaml b/checks_ext/dart_test.yaml new file mode 100644 index 00000000..6d8e711b --- /dev/null +++ b/checks_ext/dart_test.yaml @@ -0,0 +1 @@ +concurrency: 1 diff --git a/checks_ext/example/async/completer/is_completed.dart b/checks_ext/example/async/completer/is_completed.dart new file mode 100644 index 00000000..8acf2fe5 --- /dev/null +++ b/checks_ext/example/async/completer/is_completed.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final completer = Completer()..complete(42); + + /// This check succeeds. + check(completer).isCompleted(); + + /// This check fails. + final pending = Completer(); + try { + check(pending).isCompleted(); + } catch (e) { + print(e); + } + // Expected: a Completer that: + // is completed + // Actual: '> + // Which: is not completed +} diff --git a/checks_ext/example/async/streamcontroller/has_listener.dart b/checks_ext/example/async/streamcontroller/has_listener.dart new file mode 100644 index 00000000..01c5713f --- /dev/null +++ b/checks_ext/example/async/streamcontroller/has_listener.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final controller = StreamController(); + + /// This check succeeds. + check(controller).hasNoListener(); + + /// This check fails. + controller.stream.listen((_) {}); + try { + check(controller).hasNoListener(); + } catch (e) { + print(e); + } + // Expected: a StreamController that: + // has no listener + // Actual: '> + // Which: has listener +} diff --git a/checks_ext/example/async/streamcontroller/is_closed.dart b/checks_ext/example/async/streamcontroller/is_closed.dart new file mode 100644 index 00000000..31da2bd9 --- /dev/null +++ b/checks_ext/example/async/streamcontroller/is_closed.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final controller = StreamController(); + + /// This check succeeds. + check(controller).isNotClosed(); + + /// This check fails. + try { + check(controller).isClosed(); + } catch (e) { + print(e); + // Expected: a StreamController that: + // is closed + // Actual: '> + // Which: is not closed + } +} diff --git a/checks_ext/example/async/streamcontroller/is_paused.dart b/checks_ext/example/async/streamcontroller/is_paused.dart new file mode 100644 index 00000000..7a69597b --- /dev/null +++ b/checks_ext/example/async/streamcontroller/is_paused.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final controller = StreamController(); + final sub = controller.stream.listen((_) {}); + + /// This check succeeds. + check(controller).isNotPaused(); + + /// This check fails. + sub.pause(); + try { + check(controller).isNotPaused(); + } catch (e) { + print(e); + // Expected: a StreamController that: + // is not paused + // Actual: '> + // Which: is paused + } + + /* Clean up */ + sub.cancel(); +} diff --git a/checks_ext/example/core/bigint/bit_length.dart b/checks_ext/example/core/bigint/bit_length.dart new file mode 100644 index 00000000..992ffd32 --- /dev/null +++ b/checks_ext/example/core/bigint/bit_length.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(5)).bitLength.equals(3); + + /// This check fails. + try { + check(BigInt.from(5)).bitLength.equals(4); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // has bitLength that: + // equals <4> + // Actual: a BigInt that: + // has bitLength that: + // Actual: <3> + // Which: are not equal +} diff --git a/checks_ext/example/core/bigint/is_even.dart b/checks_ext/example/core/bigint/is_even.dart new file mode 100644 index 00000000..9834b00e --- /dev/null +++ b/checks_ext/example/core/bigint/is_even.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(2)).isEven(); + + /// This check fails. + try { + check(BigInt.from(1)).isEven(); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // is even + // Actual: <1> + // Which: is not even +} diff --git a/checks_ext/example/core/bigint/is_negative.dart b/checks_ext/example/core/bigint/is_negative.dart new file mode 100644 index 00000000..db760737 --- /dev/null +++ b/checks_ext/example/core/bigint/is_negative.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(-1)).isNegative(); + + /// This check fails. + try { + check(BigInt.from(1)).isNegative(); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // is negative + // Actual: <1> + // Which: is not negative +} diff --git a/checks_ext/example/core/bigint/is_not_even.dart b/checks_ext/example/core/bigint/is_not_even.dart new file mode 100644 index 00000000..b24eb8d1 --- /dev/null +++ b/checks_ext/example/core/bigint/is_not_even.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(1)).isNotEven(); + + /// This check fails. + try { + check(BigInt.from(2)).isNotEven(); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // is not even + // Actual: <2> + // Which: is even +} diff --git a/checks_ext/example/core/bigint/is_not_odd.dart b/checks_ext/example/core/bigint/is_not_odd.dart new file mode 100644 index 00000000..5faf02b3 --- /dev/null +++ b/checks_ext/example/core/bigint/is_not_odd.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(2)).isNotOdd(); + + /// This check fails. + try { + check(BigInt.from(1)).isNotOdd(); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // is not odd + // Actual: <1> + // Which: is odd +} diff --git a/checks_ext/example/core/bigint/is_odd.dart b/checks_ext/example/core/bigint/is_odd.dart new file mode 100644 index 00000000..e5c6ce2a --- /dev/null +++ b/checks_ext/example/core/bigint/is_odd.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(1)).isOdd(); + + /// This check fails. + try { + check(BigInt.from(2)).isOdd(); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // is odd + // Actual: <2> + // Which: is not odd +} diff --git a/checks_ext/example/core/bigint/is_valid_int.dart b/checks_ext/example/core/bigint/is_valid_int.dart new file mode 100644 index 00000000..14a3d587 --- /dev/null +++ b/checks_ext/example/core/bigint/is_valid_int.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(42)).isValidInt(); + + /// This check fails. + try { + check( + BigInt.parse('10000000000000000000000000000000000000000'), + ).isValidInt(); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // is valid int + // Actual: <10000000000000000000000000000000000000000> + // Which: is not valid int +} diff --git a/checks_ext/example/core/bigint/sign.dart b/checks_ext/example/core/bigint/sign.dart new file mode 100644 index 00000000..c3badee0 --- /dev/null +++ b/checks_ext/example/core/bigint/sign.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(BigInt.from(-5)).sign.equals(-1); + + /// This check fails. + try { + check(BigInt.from(-5)).sign.equals(1); + } catch (e) { + print(e); + } + // Expected: a BigInt that: + // has sign that: + // equals <1> + // Actual: a BigInt that: + // has sign that: + // Actual: <-1> + // Which: are not equal +} diff --git a/checks_ext/example/core/comparable/average.dart b/checks_ext/example/core/comparable/average.dart new file mode 100644 index 00000000..20dde4ee --- /dev/null +++ b/checks_ext/example/core/comparable/average.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 3]).average.equals(2.0); + + /// This check fails. + try { + check([1, 2, 3]).average.equals(3.0); + } catch (e) { + print(e); + } + // Expected: a List that: + // has average that: + // equals <3.0> + // Actual: a List that: + // has average that: + // Actual: <2.0> + // Which: are not equal +} diff --git a/checks_ext/example/core/comparable/is_sorted.dart b/checks_ext/example/core/comparable/is_sorted.dart new file mode 100644 index 00000000..e6ddd3b1 --- /dev/null +++ b/checks_ext/example/core/comparable/is_sorted.dart @@ -0,0 +1,18 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 3]).isSorted(); + + /// This check fails. + try { + check([2, 1, 3]).isSorted(); + } catch (e) { + print(e); + } + // Expected: a List that: + // is sorted + // Actual: [2, 1, 3] + // Which: is not sorted, because element at index 0 <2> + // compares after element at index 1 <1> +} diff --git a/checks_ext/example/core/comparable/max.dart b/checks_ext/example/core/comparable/max.dart new file mode 100644 index 00000000..60997ae1 --- /dev/null +++ b/checks_ext/example/core/comparable/max.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 5, 3]).max.equals(5); + + /// This check fails. + try { + check([1, 5, 3]).max.equals(1); + } catch (e) { + print(e); + // Expected: a List that: + // has max that: + // equals <1> + // Actual: a List that: + // has max that: + // Actual: <5> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/comparable/min.dart b/checks_ext/example/core/comparable/min.dart new file mode 100644 index 00000000..1f1c21d3 --- /dev/null +++ b/checks_ext/example/core/comparable/min.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([5, 1, 3]).min.equals(1); + + /// This check fails. + try { + check([5, 1, 3]).min.equals(5); + } catch (e) { + print(e); + // Expected: a List that: + // has min that: + // equals <5> + // Actual: a List that: + // has min that: + // Actual: <1> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/comparable/sum.dart b/checks_ext/example/core/comparable/sum.dart new file mode 100644 index 00000000..211b6cd6 --- /dev/null +++ b/checks_ext/example/core/comparable/sum.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 3]).sum.equals(6); + + /// This check fails. + try { + check([1, 2, 3]).sum.equals(5); + } catch (e) { + print(e); + } + // Expected: a List that: + // has sum that: + // equals <5> + // Actual: a List that: + // has sum that: + // Actual: <6> + // Which: are not equal +} diff --git a/checks_ext/example/core/datetime/day.dart b/checks_ext/example/core/datetime/day.dart new file mode 100644 index 00000000..53ea01a9 --- /dev/null +++ b/checks_ext/example/core/datetime/day.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).day.equals(2); + + /// This check fails. + try { + check(date).day.equals(15); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has day that: + // equals <15> + // Actual: a DateTime that: + // has day that: + // Actual: <2> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/difference.dart b/checks_ext/example/core/datetime/difference.dart new file mode 100644 index 00000000..63321356 --- /dev/null +++ b/checks_ext/example/core/datetime/difference.dart @@ -0,0 +1,23 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date1 = DateTime(2026, 6, 2); + final date2 = DateTime(2026, 6, 1); + + /// This check succeeds. + check(date1).difference(date2).equals(Duration(days: 1)); + + /// This check fails. + try { + check(date1).difference(date2).equals(Duration(days: 2)); + } catch (e) { + print(e); + } + // Expected: a DateTime that: + // difference from <2026-06-01 00:00:00.000> that: + // equals <48:00:00.000000> + // Actual: a DateTime that: + // difference from <2026-06-01 00:00:00.000> that: + // Actual: <24:00:00.000000> + // Which: are not equal +} diff --git a/checks_ext/example/core/datetime/hour.dart b/checks_ext/example/core/datetime/hour.dart new file mode 100644 index 00000000..c958160f --- /dev/null +++ b/checks_ext/example/core/datetime/hour.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2, 14); // 14:00 + + /// This check succeeds. + check(date).hour.equals(14); + + /// This check fails. + try { + check(date).hour.equals(9); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has hour that: + // equals <9> + // Actual: a DateTime that: + // has hour that: + // Actual: <14> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/is_after.dart b/checks_ext/example/core/datetime/is_after.dart new file mode 100644 index 00000000..043ae963 --- /dev/null +++ b/checks_ext/example/core/datetime/is_after.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).isAfter(DateTime(2026, 6, 1)); + + /// This check fails. + try { + check(date).isAfter(DateTime(2026, 6, 3)); + } catch (e) { + print(e); + // Expected: a DateTime that: + // is after <2026-06-03 00:00:00.000> + // Actual: <2026-06-02 00:00:00.000> + // Which: was not after <2026-06-03 00:00:00.000> + } +} diff --git a/checks_ext/example/core/datetime/is_at_same_moment_as.dart b/checks_ext/example/core/datetime/is_at_same_moment_as.dart new file mode 100644 index 00000000..03f6c65d --- /dev/null +++ b/checks_ext/example/core/datetime/is_at_same_moment_as.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).isAtSameMomentAs(DateTime(2026, 6, 2)); + + /// This check fails. + try { + check(date).isAtSameMomentAs(DateTime(2026, 6, 1)); + } catch (e) { + print(e); + // Expected: a DateTime that: + // is at same moment as <2026-06-01 00:00:00.000> + // Actual: <2026-06-02 00:00:00.000> + // Which: was not at same moment as <2026-06-01 00:00:00.000> + } +} diff --git a/checks_ext/example/core/datetime/is_before.dart b/checks_ext/example/core/datetime/is_before.dart new file mode 100644 index 00000000..8520a913 --- /dev/null +++ b/checks_ext/example/core/datetime/is_before.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).isBefore(DateTime(2026, 6, 3)); + + /// This check fails. + try { + check(date).isBefore(DateTime(2026, 6, 1)); + } catch (e) { + print(e); + // Expected: a DateTime that: + // is before <2026-06-01 00:00:00.000> + // Actual: <2026-06-02 00:00:00.000> + // Which: was not before <2026-06-01 00:00:00.000> + } +} diff --git a/checks_ext/example/core/datetime/is_between.dart b/checks_ext/example/core/datetime/is_between.dart new file mode 100644 index 00000000..859c8a02 --- /dev/null +++ b/checks_ext/example/core/datetime/is_between.dart @@ -0,0 +1,23 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).isBetween(DateTime(2026, 6, 1), DateTime(2026, 6, 3)); + + /// This check fails. + try { + check(date).isBetween(DateTime(2026, 6, 4), DateTime(2026, 6, 6)); + } catch (e) { + print(e); + // Expected: a DateTime that: + // is between <2026-06-04 00:00:00.000> + // and <2026-06-06 00:00:00.000> + // (inclusive) + // Actual: <2026-06-02 00:00:00.000> + // Which: was not between <2026-06-04 00:00:00.000> + // and <2026-06-06 00:00:00.000> + // (inclusive) + } +} diff --git a/checks_ext/example/core/datetime/is_close_to.dart b/checks_ext/example/core/datetime/is_close_to.dart new file mode 100644 index 00000000..b3e55e06 --- /dev/null +++ b/checks_ext/example/core/datetime/is_close_to.dart @@ -0,0 +1,21 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date1 = DateTime(2026, 6, 2); + final date2 = DateTime(2026, 6, 1); + + /// This check succeeds. + check(date1).isCloseTo(date2, within: Duration(days: 2)); + + /// This check fails. + try { + check(date1).isCloseTo(date2, within: Duration(hours: 1)); + } catch (e) { + print(e); + } + // Expected: a DateTime that: + // is close to <2026-06-01 00:00:00.000> within 1 hour + // Actual: <2026-06-02 00:00:00.000> + // Which: differs by 1 day + // which is more than 1 hour +} diff --git a/checks_ext/example/core/datetime/is_not_utc.dart b/checks_ext/example/core/datetime/is_not_utc.dart new file mode 100644 index 00000000..d473bede --- /dev/null +++ b/checks_ext/example/core/datetime/is_not_utc.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).isNotUtc(); + + /// This check fails. + try { + check(DateTime.utc(2026, 6, 2)).isNotUtc(); + } catch (e) { + print(e); + // Expected: a DateTime that: + // is not in UTC + // Actual: <2026-06-02 00:00:00.000Z> + // Which: is UTC + } +} diff --git a/checks_ext/example/core/datetime/is_utc.dart b/checks_ext/example/core/datetime/is_utc.dart new file mode 100644 index 00000000..4246dd27 --- /dev/null +++ b/checks_ext/example/core/datetime/is_utc.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime.utc(2026, 6, 2); + + /// This check succeeds. + check(date).isUtc(); + + /// This check fails. + try { + check(DateTime(2026, 6, 2)).isUtc(); + } catch (e) { + print(e); + // Expected: a DateTime that: + // is UTC + // Actual: <2026-06-02 00:00:00.000> + // Which: is not in UTC + } +} diff --git a/checks_ext/example/core/datetime/microseconds_since_epoch.dart b/checks_ext/example/core/datetime/microseconds_since_epoch.dart new file mode 100644 index 00000000..4a532e4a --- /dev/null +++ b/checks_ext/example/core/datetime/microseconds_since_epoch.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime.fromMicrosecondsSinceEpoch(123456789); + + /// This check succeeds. + check(date).microsecondsSinceEpoch.equals(123456789); + + /// This check fails. + try { + check(date).microsecondsSinceEpoch.equals(0); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has microsecondsSinceEpoch that: + // equals <0> + // Actual: a DateTime that: + // has microsecondsSinceEpoch that: + // Actual: <123456789> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/minute.dart b/checks_ext/example/core/datetime/minute.dart new file mode 100644 index 00000000..351a2d0b --- /dev/null +++ b/checks_ext/example/core/datetime/minute.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2, 14, 30); // 14:30 + + /// This check succeeds. + check(date).minute.equals(30); + + /// This check fails. + try { + check(date).minute.equals(0); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has minute that: + // equals <0> + // Actual: a DateTime that: + // has minute that: + // Actual: <30> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/month.dart b/checks_ext/example/core/datetime/month.dart new file mode 100644 index 00000000..00148e97 --- /dev/null +++ b/checks_ext/example/core/datetime/month.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).month.equals(6); + + /// This check fails. + try { + check(date).month.equals(12); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has month that: + // equals <12> + // Actual: a DateTime that: + // has month that: + // Actual: <6> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/second.dart b/checks_ext/example/core/datetime/second.dart new file mode 100644 index 00000000..071fe250 --- /dev/null +++ b/checks_ext/example/core/datetime/second.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2, 14, 30, 45); // 14:30:45 + + /// This check succeeds. + check(date).second.equals(45); + + /// This check fails. + try { + check(date).second.equals(0); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has second that: + // equals <0> + // Actual: a DateTime that: + // has second that: + // Actual: <45> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/weekday.dart b/checks_ext/example/core/datetime/weekday.dart new file mode 100644 index 00000000..e3e57271 --- /dev/null +++ b/checks_ext/example/core/datetime/weekday.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); // Tuesday (2) + + /// This check succeeds. + check(date).weekday.equals(DateTime.tuesday); + + /// This check fails. + try { + check(date).weekday.equals(DateTime.sunday); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has weekday that: + // equals <7> + // Actual: a DateTime that: + // has weekday that: + // Actual: <2> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/datetime/year.dart b/checks_ext/example/core/datetime/year.dart new file mode 100644 index 00000000..e3b8a630 --- /dev/null +++ b/checks_ext/example/core/datetime/year.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final date = DateTime(2026, 6, 2); + + /// This check succeeds. + check(date).year.equals(2026); + + /// This check fails. + try { + check(date).year.equals(2020); + } catch (e) { + print(e); + // Expected: a DateTime that: + // has year that: + // equals <2020> + // Actual: a DateTime that: + // has year that: + // Actual: <2026> + // Which: are not equal + } +} diff --git a/checks_ext/example/core/duration/is_close_to.dart b/checks_ext/example/core/duration/is_close_to.dart new file mode 100644 index 00000000..aaaf1290 --- /dev/null +++ b/checks_ext/example/core/duration/is_close_to.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check( + Duration(seconds: 5), + ).isCloseTo(Duration(seconds: 6), within: Duration(seconds: 2)); + + /// This check fails. + try { + check( + Duration(seconds: 5), + ).isCloseTo(Duration(seconds: 8), within: Duration(seconds: 2)); + } catch (e) { + print(e); + } + // Expected: a Duration that: + // is close to 8s within 2s + // Actual: <0:00:05.000000> + // Which: differs by 3s + // which is more than 2s +} diff --git a/checks_ext/example/core/duration/is_greater_than.dart b/checks_ext/example/core/duration/is_greater_than.dart new file mode 100644 index 00000000..7fa10ce1 --- /dev/null +++ b/checks_ext/example/core/duration/is_greater_than.dart @@ -0,0 +1,18 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(Duration(seconds: 5)).isGreaterThan(Duration(seconds: 2)); + + /// This check fails. + try { + check(Duration(seconds: 1)).isGreaterThan(Duration(seconds: 2)); + } catch (e) { + print(e); + } + // Expected: a Duration that: + // is greater than 2s + // Actual: <0:00:01.000000> + // Which: was not greater than 2s + // actual was 1s +} diff --git a/checks_ext/example/core/duration/is_less_than.dart b/checks_ext/example/core/duration/is_less_than.dart new file mode 100644 index 00000000..c0367ded --- /dev/null +++ b/checks_ext/example/core/duration/is_less_than.dart @@ -0,0 +1,18 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(Duration(seconds: 1)).isLessThan(Duration(seconds: 2)); + + /// This check fails. + try { + check(Duration(seconds: 3)).isLessThan(Duration(seconds: 2)); + } catch (e) { + print(e); + } + // Expected: a Duration that: + // is less than 2s + // Actual: <0:00:03.000000> + // Which: was not less than 2s + // actual was 3s +} diff --git a/checks_ext/example/core/duration/is_negative.dart b/checks_ext/example/core/duration/is_negative.dart new file mode 100644 index 00000000..b7294274 --- /dev/null +++ b/checks_ext/example/core/duration/is_negative.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(Duration(seconds: -5)).isNegative(); + + /// This check fails. + try { + check(Duration(seconds: 5)).isNegative(); + } catch (e) { + print(e); + } + // Expected: a Duration that: + // is negative + // Actual: <0:00:05.000000> + // Which: is not negative +} diff --git a/checks_ext/example/core/duration/is_zero.dart b/checks_ext/example/core/duration/is_zero.dart new file mode 100644 index 00000000..4df8a338 --- /dev/null +++ b/checks_ext/example/core/duration/is_zero.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(Duration.zero).isZero(); + + /// This check fails. + try { + check(Duration(seconds: 5)).isZero(); + } catch (e) { + print(e); + } + // Expected: a Duration that: + // is zero + // Actual: <0:00:05.000000> + // Which: is not zero +} diff --git a/checks_ext/example/core/enum/name.dart b/checks_ext/example/core/enum/name.dart new file mode 100644 index 00000000..5333542f --- /dev/null +++ b/checks_ext/example/core/enum/name.dart @@ -0,0 +1,25 @@ +import 'package:checks_ext/checks_ext.dart'; + +enum Fruit { apple, banana } + +void main() { + /// This check succeeds. + check(Fruit.apple).name.equals('apple'); + + /// This check fails. + try { + check(Fruit.apple).name.equals('banana'); + } catch (e) { + print(e); + } + // Expected: a Fruit that: + // has name that: + // equals 'banana' + // Actual: a Fruit that: + // has name that: + // Actual: 'apple' + // Which: differs at offset 0: + // banana + // apple + // ^ +} diff --git a/checks_ext/example/core/iterable/contains_all.dart b/checks_ext/example/core/iterable/contains_all.dart new file mode 100644 index 00000000..96d4a522 --- /dev/null +++ b/checks_ext/example/core/iterable/contains_all.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 3]).containsAll([1, 2]); + + /// This check fails. + try { + check([1, 2, 3]).containsAll([1, 4]); + } catch (e) { + print(e); + } + // Expected: a List that: + // contains all of [1, 4] + // Actual: [1, 2, 3] + // Which: is missing elements: [4] +} diff --git a/checks_ext/example/core/iterable/contains_sequence.dart b/checks_ext/example/core/iterable/contains_sequence.dart new file mode 100644 index 00000000..ac8d29bb --- /dev/null +++ b/checks_ext/example/core/iterable/contains_sequence.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 3, 4]).containsSequence([2, 3]); + + /// This check fails. + try { + check([1, 2, 3, 4]).containsSequence([2, 4]); + } catch (e) { + print(e); + } + // Expected: a List that: + // contains sequence [2, 4] + // Actual: [1, 2, 3, 4] + // Which: does not contain the sequence +} diff --git a/checks_ext/example/core/iterable/has_duplicates.dart b/checks_ext/example/core/iterable/has_duplicates.dart new file mode 100644 index 00000000..5f60b86d --- /dev/null +++ b/checks_ext/example/core/iterable/has_duplicates.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 1]).hasDuplicates(); + + /// This check fails. + try { + check([1, 2, 3]).hasDuplicates(); + } catch (e) { + print(e); + } + // Expected: a List that: + // has duplicates + // Actual: [1, 2, 3] + // Which: has no duplicates +} diff --git a/checks_ext/example/core/iterable/has_no_duplicates.dart b/checks_ext/example/core/iterable/has_no_duplicates.dart new file mode 100644 index 00000000..f7293e27 --- /dev/null +++ b/checks_ext/example/core/iterable/has_no_duplicates.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, 2, 3]).hasNoDuplicates(); + + /// This check fails. + try { + check([1, 2, 1, 2]).hasNoDuplicates(); + } catch (e) { + print(e); + } + // Expected: a List that: + // has no duplicates + // Actual: [1, 2, 1, 2] + // Which: has duplicates: [1, 2] +} diff --git a/checks_ext/example/core/iterable/is_sorted_by.dart b/checks_ext/example/core/iterable/is_sorted_by.dart new file mode 100644 index 00000000..5c4e0a8c --- /dev/null +++ b/checks_ext/example/core/iterable/is_sorted_by.dart @@ -0,0 +1,31 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final people = [ + (name: 'Alice', age: 25), + (name: 'Bob', age: 30), + (name: 'Charlie', age: 35), + ]; + + /// This check succeeds. + check(people).isSortedBy((p) => p.age); + + /// This check fails. + final unsorted = [ + (name: 'Charlie', age: 35), + (name: 'Bob', age: 30), + (name: 'Alice', age: 25), + ]; + try { + check(unsorted).isSortedBy((p) => p.age); + } catch (e) { + print(e); + // Expected: a List<({int age, String name})> that: + // is sorted by key + // Actual: [(age: 35, name: Charlie), (age: 30, name: Bob), (age: 25, name: Alice)] + // Which: is not sorted, because element at index 0 <(age: 35, name: Charlie)> + // with key <35> + // compares after element at index 1 <(age: 30, name: Bob)> + // with key <30> + } +} diff --git a/checks_ext/example/core/iterable/non_nulls.dart b/checks_ext/example/core/iterable/non_nulls.dart new file mode 100644 index 00000000..7c65aa7b --- /dev/null +++ b/checks_ext/example/core/iterable/non_nulls.dart @@ -0,0 +1,21 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([1, null, 2]).nonNulls.deepEquals([1, 2]); + + /// This check fails. + try { + check([1, null, 2]).nonNulls.deepEquals([1, 3]); + } catch (e) { + print(e); + } + // Expected: a List that: + // skipping nulls that: + // is deeply equal to [1, 3] + // Actual: a List that: + // skipping nulls that: + // Actual: (1, 2) + // Which: at [<1>] is <2> + // which does not equal <3> +} diff --git a/checks_ext/example/core/list/as_utf8.dart b/checks_ext/example/core/list/as_utf8.dart new file mode 100644 index 00000000..8be84192 --- /dev/null +++ b/checks_ext/example/core/list/as_utf8.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final bytes = [104, 101, 108, 108, 111]; // 'hello' + + /// This check succeeds. + check(bytes).asUtf8.equals('hello'); + + /// This check fails. + final invalidBytes = [255, 254]; + try { + check(invalidBytes).asUtf8.equals('hello'); + } catch (e) { + print(e); + // Expected: a List that: + // as UTF-8 string + // Actual: [255, 254] + // Which: is not valid UTF-8 + } +} diff --git a/checks_ext/example/core/list/contains_bytes.dart b/checks_ext/example/core/list/contains_bytes.dart new file mode 100644 index 00000000..4d4a4d99 --- /dev/null +++ b/checks_ext/example/core/list/contains_bytes.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final bytes = [1, 2, 3, 4, 5]; + + /// This check succeeds. + check(bytes).containsBytes([3, 4]); + + /// This check fails. + try { + check(bytes).containsBytes([3, 6]); + } catch (e) { + print(e); + } + // Expected: a List that: + // contains bytes [3, 6] + // Actual: [1, 2, 3, 4, 5] + // Which: does not contain the sequence: 03 06 + // Actual bytes: 01 02 03 04 05 +} diff --git a/checks_ext/example/core/list/equals_bytes.dart b/checks_ext/example/core/list/equals_bytes.dart new file mode 100644 index 00000000..dc18a110 --- /dev/null +++ b/checks_ext/example/core/list/equals_bytes.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check([0, 1, 2]).equalsBytes([0, 1, 2]); + + /// This check fails showing hex output. + try { + check([0, 1, 2]).equalsBytes([0, 1, 3]); + } catch (e) { + print(e); + } + // Expected: a List that: + // equals [0, 1, 3] + // Actual: [0, 1, 2] + // Which: differs at offset 2: + // Actual: 00 01 [02] + // Expected: 00 01 [03] +} diff --git a/checks_ext/example/core/list/is_valid_utf8.dart b/checks_ext/example/core/list/is_valid_utf8.dart new file mode 100644 index 00000000..fd924d41 --- /dev/null +++ b/checks_ext/example/core/list/is_valid_utf8.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final bytes = [104, 101, 108, 108, 111]; // 'hello' + + /// This check succeeds. + check(bytes).isValidUtf8(); + + /// This check fails. + final invalidBytes = [255, 254]; + try { + check(invalidBytes).isValidUtf8(); + } catch (e) { + print(e); + // Expected: a List that: + // is valid UTF-8 + // Actual: [255, 254] + // Which: is not valid UTF-8 + } +} diff --git a/checks_ext/example/core/mapentry/key.dart b/checks_ext/example/core/mapentry/key.dart new file mode 100644 index 00000000..d1e7fc44 --- /dev/null +++ b/checks_ext/example/core/mapentry/key.dart @@ -0,0 +1,25 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final entry = MapEntry('a', 1); + + /// This check succeeds. + check(entry).key.equals('a'); + + /// This check fails. + try { + check(entry).key.equals('b'); + } catch (e) { + print(e); + } + // Expected: a MapEntry that: + // has key that: + // equals 'b' + // Actual: a MapEntry that: + // has key that: + // Actual: 'a' + // Which: differs at offset 0: + // b + // a + // ^ +} diff --git a/checks_ext/example/core/mapentry/value.dart b/checks_ext/example/core/mapentry/value.dart new file mode 100644 index 00000000..66309430 --- /dev/null +++ b/checks_ext/example/core/mapentry/value.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final entry = MapEntry('a', 1); + + /// This check succeeds. + check(entry).value.equals(1); + + /// This check fails. + try { + check(entry).value.equals(2); + } catch (e) { + print(e); + } + // Expected: a MapEntry that: + // has value that: + // equals <2> + // Actual: a MapEntry that: + // has value that: + // Actual: <1> + // Which: are not equal +} diff --git a/checks_ext/example/core/match/group.dart b/checks_ext/example/core/match/group.dart new file mode 100644 index 00000000..98e6e76a --- /dev/null +++ b/checks_ext/example/core/match/group.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final match = RegExp(r'(\d+)').firstMatch('123')!; + + /// This check succeeds. + check(match)[1].equals('123'); + + /// This check fails. + try { + check(match)[1].equals('456'); + } catch (e) { + print(e); + } + // Expected: a RegExpMatch that: + // has group(1) that: + // equals '456' + // Actual: a RegExpMatch that: + // has group(1) that: + // Actual: '123' + // Which: are not equal +} diff --git a/checks_ext/example/core/match/start.dart b/checks_ext/example/core/match/start.dart new file mode 100644 index 00000000..37b828b8 --- /dev/null +++ b/checks_ext/example/core/match/start.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final match = RegExp(r'banana').firstMatch('apple banana cherry')!; + + /// This check succeeds. + check(match).start.equals(6); + + /// This check fails. + try { + check(match).start.equals(0); + } catch (e) { + print(e); + } + // Expected: a RegExpMatch that: + // has start that: + // equals <0> + // Actual: a RegExpMatch that: + // has start that: + // Actual: <6> + // Which: are not equal +} diff --git a/checks_ext/example/core/num/is_between.dart b/checks_ext/example/core/num/is_between.dart new file mode 100644 index 00000000..b452a59e --- /dev/null +++ b/checks_ext/example/core/num/is_between.dart @@ -0,0 +1,21 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(5).isBetween(1, 10); + + /// This check fails. + try { + check(0).isBetween(1, 10); + } catch (e) { + print(e); + } + // Expected: a int that: + // is between <1> + // and <10> + // (inclusive) + // Actual: <0> + // Which: is not between <1> + // and <10> + // (inclusive) +} diff --git a/checks_ext/example/core/num/is_even.dart b/checks_ext/example/core/num/is_even.dart new file mode 100644 index 00000000..4616eca8 --- /dev/null +++ b/checks_ext/example/core/num/is_even.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(2).isEven(); + + /// This check fails. + try { + check(1).isEven(); + } catch (e) { + print(e); + } + // Expected: a int that: + // is even + // Actual: <1> + // Which: is odd +} diff --git a/checks_ext/example/core/num/is_multiple_of.dart b/checks_ext/example/core/num/is_multiple_of.dart new file mode 100644 index 00000000..a5fc1918 --- /dev/null +++ b/checks_ext/example/core/num/is_multiple_of.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(6).isMultipleOf(3); + + /// This check fails. + try { + check(5).isMultipleOf(3); + } catch (e) { + print(e); + } + // Expected: a int that: + // is multiple of <3> + // Actual: <5> + // Which: is not a multiple of <3> +} diff --git a/checks_ext/example/core/num/is_strictly_positive.dart b/checks_ext/example/core/num/is_strictly_positive.dart new file mode 100644 index 00000000..6f1b3675 --- /dev/null +++ b/checks_ext/example/core/num/is_strictly_positive.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(1).isStrictlyPositive(); + + /// This check fails. + try { + check(0).isStrictlyPositive(); + } catch (e) { + print(e); + } + // Expected: a int that: + // is strictly positive + // Actual: <0> + // Which: is not strictly positive +} diff --git a/checks_ext/example/core/regexp/all_matches.dart b/checks_ext/example/core/regexp/all_matches.dart new file mode 100644 index 00000000..e82d7c1d --- /dev/null +++ b/checks_ext/example/core/regexp/all_matches.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final regex = RegExp(r'\d+'); + + /// This check succeeds. + check(regex).allMatches('123 456').length.equals(2); + + /// This check fails. + try { + check(regex).allMatches('abc').isNotEmpty(); + } catch (e) { + print(e); + } + // Expected: a RegExp that: + // has allMatches on 'abc' that: + // is not empty + // Actual: a RegExp that: + // has allMatches on 'abc' that: + // Actual: () + // Which: is empty +} diff --git a/checks_ext/example/core/regexp/first_match.dart b/checks_ext/example/core/regexp/first_match.dart new file mode 100644 index 00000000..e748ec03 --- /dev/null +++ b/checks_ext/example/core/regexp/first_match.dart @@ -0,0 +1,21 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final regex = RegExp(r'(?\d{4})'); + + /// This check succeeds. + check(regex).firstMatch('2026').isNotNull().namedGroup('year').equals('2026'); + + /// This check fails. + try { + check(regex).firstMatch('abc').isNotNull(); + } catch (e) { + print(e); + } + // Expected: a RegExp that: + // has firstMatch on 'abc' that: + // is not null + // Actual: a RegExp that: + // has firstMatch on 'abc' that: + // Actual: +} diff --git a/checks_ext/example/core/regexp/has_match.dart b/checks_ext/example/core/regexp/has_match.dart new file mode 100644 index 00000000..9456ebaf --- /dev/null +++ b/checks_ext/example/core/regexp/has_match.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final regex = RegExp(r'\d+'); + + /// This check succeeds. + check(regex).hasMatch('123'); + + /// This check fails. + try { + check(regex).hasMatch('abc'); + } catch (e) { + print(e); + } + // Expected: a RegExp that: + // matches 'abc' + // Actual: + // Which: does not match 'abc' +} diff --git a/checks_ext/example/core/regexp/has_no_match.dart b/checks_ext/example/core/regexp/has_no_match.dart new file mode 100644 index 00000000..17228582 --- /dev/null +++ b/checks_ext/example/core/regexp/has_no_match.dart @@ -0,0 +1,21 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final regex = RegExp(r'\d+'); + + /// This check succeeds. + check(regex).hasNoMatch('abc'); + + /// This check fails. + try { + check(regex).hasNoMatch('123'); + } catch (e) { + print(e); + } + // Expected: a RegExp that: + // does not match '123' + // Actual: + // Which: matched '123' at index 0: + // 123 + // ^ +} diff --git a/checks_ext/example/core/regexp/pattern.dart b/checks_ext/example/core/regexp/pattern.dart new file mode 100644 index 00000000..c4ac06ad --- /dev/null +++ b/checks_ext/example/core/regexp/pattern.dart @@ -0,0 +1,25 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final regex = RegExp(r'\d+'); + + /// This check succeeds. + check(regex).pattern.equals(r'\d+'); + + /// This check fails. + try { + check(regex).pattern.equals(r'\w+'); + } catch (e) { + print(e); + } + // Expected: a RegExp that: + // has pattern that: + // equals '\\w+' + // Actual: a RegExp that: + // has pattern that: + // Actual: '\\d+' + // Which: differs at offset 2: + // \\w+ + // \\d+ + // ^ +} diff --git a/checks_ext/example/core/regexpmatch/named_group.dart b/checks_ext/example/core/regexpmatch/named_group.dart new file mode 100644 index 00000000..e0dd62b5 --- /dev/null +++ b/checks_ext/example/core/regexpmatch/named_group.dart @@ -0,0 +1,22 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final match = RegExp(r'(?\d{4})').firstMatch('2026')!; + + /// This check succeeds. + check(match).namedGroup('year').equals('2026'); + + /// This check fails. + try { + check(match).namedGroup('year').equals('2025'); + } catch (e) { + print(e); + } + // Expected: a RegExpMatch that: + // has namedGroup(year) that: + // equals '2025' + // Actual: a RegExpMatch that: + // has namedGroup(year) that: + // Actual: '2026' + // Which: are not equal +} diff --git a/checks_ext/example/core/set/is_subset_of.dart b/checks_ext/example/core/set/is_subset_of.dart new file mode 100644 index 00000000..a2887c4d --- /dev/null +++ b/checks_ext/example/core/set/is_subset_of.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check({1, 2}).isSubsetOf({1, 2, 3}); + + /// This check fails. + try { + check({1, 4}).isSubsetOf({1, 2, 3}); + } catch (e) { + print(e); + } + // Expected: a Set that: + // is a subset of {1, 2, 3} + // Actual: {1, 4} + // Which: contains elements not in the other set: {4} +} diff --git a/checks_ext/example/core/stopwatch/is_running.dart b/checks_ext/example/core/stopwatch/is_running.dart new file mode 100644 index 00000000..2f393da0 --- /dev/null +++ b/checks_ext/example/core/stopwatch/is_running.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final stopwatch = Stopwatch()..start(); + + /// This check succeeds. + check(stopwatch).isRunning(); + + /// This check fails. + final stopped = Stopwatch(); + try { + check(stopped).isRunning(); + } catch (e) { + print(e); + } + // Expected: a Stopwatch that: + // is running + // Actual: Stopwatch(running: false, elapsed: 0s) + // Which: is not running +} diff --git a/checks_ext/example/core/string/contains_ignoring_case.dart b/checks_ext/example/core/string/contains_ignoring_case.dart new file mode 100644 index 00000000..24673dc0 --- /dev/null +++ b/checks_ext/example/core/string/contains_ignoring_case.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('HelloWorld').containsIgnoringCase('world'); + + /// This check fails. + try { + check('Hello').containsIgnoringCase('world'); + } catch (e) { + print(e); + } + // Expected: a String that: + // contains ignoring case 'world' + // Actual: 'Hello' + // Which: does not contain 'world' (case-insensitive) +} diff --git a/checks_ext/example/core/string/is_alpha.dart b/checks_ext/example/core/string/is_alpha.dart new file mode 100644 index 00000000..9004ae60 --- /dev/null +++ b/checks_ext/example/core/string/is_alpha.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('abc').isAlpha(); + + /// This check fails. + try { + check('ab1').isAlpha(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is alpha + // Actual: 'ab1' + // Which: contains non-alphabetic characters +} diff --git a/checks_ext/example/core/string/is_alphanumeric.dart b/checks_ext/example/core/string/is_alphanumeric.dart new file mode 100644 index 00000000..7fadf9ca --- /dev/null +++ b/checks_ext/example/core/string/is_alphanumeric.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('ab12').isAlphanumeric(); + + /// This check fails. + try { + check('ab1!').isAlphanumeric(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is alphanumeric + // Actual: 'ab1!' + // Which: contains characters that are not letters or digits +} diff --git a/checks_ext/example/core/string/is_blank.dart b/checks_ext/example/core/string/is_blank.dart new file mode 100644 index 00000000..ef0f11f4 --- /dev/null +++ b/checks_ext/example/core/string/is_blank.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(' ').isBlank(); + + /// This check fails. + try { + check('abc').isBlank(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is blank + // Actual: 'abc' + // Which: was not blank +} diff --git a/checks_ext/example/core/string/is_capitalized.dart b/checks_ext/example/core/string/is_capitalized.dart new file mode 100644 index 00000000..a7c9f541 --- /dev/null +++ b/checks_ext/example/core/string/is_capitalized.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('Apple').isCapitalized(); + + /// This check fails. + try { + check('apple').isCapitalized(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is capitalized + // Actual: 'apple' + // Which: first character was not upper case +} diff --git a/checks_ext/example/core/string/is_lower_case.dart b/checks_ext/example/core/string/is_lower_case.dart new file mode 100644 index 00000000..4475ad21 --- /dev/null +++ b/checks_ext/example/core/string/is_lower_case.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('abc').isLowerCase(); + + /// This check fails. + try { + check('Abc').isLowerCase(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is lower case + // Actual: 'Abc' + // Which: was not lower case +} diff --git a/checks_ext/example/core/string/is_not_blank.dart b/checks_ext/example/core/string/is_not_blank.dart new file mode 100644 index 00000000..d348235f --- /dev/null +++ b/checks_ext/example/core/string/is_not_blank.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('abc').isNotBlank(); + + /// This check fails. + try { + check(' ').isNotBlank(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is not blank + // Actual: ' ' + // Which: was blank +} diff --git a/checks_ext/example/core/string/is_numeric.dart b/checks_ext/example/core/string/is_numeric.dart new file mode 100644 index 00000000..83fbb6c8 --- /dev/null +++ b/checks_ext/example/core/string/is_numeric.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('123').isNumeric(); + + /// This check fails. + try { + check('12a').isNumeric(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is numeric + // Actual: '12a' + // Which: contains non-numeric characters +} diff --git a/checks_ext/example/core/string/is_upper_case.dart b/checks_ext/example/core/string/is_upper_case.dart new file mode 100644 index 00000000..e053f4c5 --- /dev/null +++ b/checks_ext/example/core/string/is_upper_case.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('ABC').isUpperCase(); + + /// This check fails. + try { + check('Abc').isUpperCase(); + } catch (e) { + print(e); + } + // Expected: a String that: + // is upper case + // Actual: 'Abc' + // Which: was not upper case +} diff --git a/checks_ext/example/core/string/lines.dart b/checks_ext/example/core/string/lines.dart new file mode 100644 index 00000000..74f42064 --- /dev/null +++ b/checks_ext/example/core/string/lines.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check('apple\nbanana\ncherry').lines.contains('banana'); + + /// This check fails. + try { + check('apple\nbanana\ncherry').lines.contains('date'); + } catch (e) { + print(e); + } + // Expected: a String that: + // has lines that: + // contains 'date' + // Actual: a String that: + // has lines that: + // Actual: ['apple', 'banana', 'cherry'] + // Which: does not contain 'date' +} diff --git a/checks_ext/example/core/stringbuffer/as_string.dart b/checks_ext/example/core/stringbuffer/as_string.dart new file mode 100644 index 00000000..fac9d9ed --- /dev/null +++ b/checks_ext/example/core/stringbuffer/as_string.dart @@ -0,0 +1,25 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = StringBuffer('abc'); + + /// This check succeeds. + check(buffer).asString.equals('abc'); + + /// This check fails. + try { + check(buffer).asString.equals('def'); + } catch (e) { + print(e); + } + // Expected: a StringBuffer that: + // has asString that: + // equals 'def' + // Actual: a StringBuffer that: + // has asString that: + // Actual: 'abc' + // Which: differs at offset 0: + // def + // abc + // ^ +} diff --git a/checks_ext/example/core/stringbuffer/is_empty.dart b/checks_ext/example/core/stringbuffer/is_empty.dart new file mode 100644 index 00000000..38ec3e6f --- /dev/null +++ b/checks_ext/example/core/stringbuffer/is_empty.dart @@ -0,0 +1,17 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(StringBuffer()).isEmpty(); + + /// This check fails. + try { + check(StringBuffer('abc')).isEmpty(); + } catch (e) { + print(e); + } + // Expected: a StringBuffer that: + // is empty + // Actual: + // Which: is not empty +} diff --git a/checks_ext/example/core/uri/has_scheme.dart b/checks_ext/example/core/uri/has_scheme.dart new file mode 100644 index 00000000..b5ee2440 --- /dev/null +++ b/checks_ext/example/core/uri/has_scheme.dart @@ -0,0 +1,19 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final uri = Uri.parse('https://example.com'); + + /// This check succeeds. + check(uri).hasScheme('https'); + + /// This check fails. + try { + check(uri).hasScheme('http'); + } catch (e) { + print(e); + } + // Expected: a Uri that: + // has scheme 'http' + // Actual: + // Which: has scheme 'https' +} diff --git a/checks_ext/example/core/uri/host.dart b/checks_ext/example/core/uri/host.dart new file mode 100644 index 00000000..710ed037 --- /dev/null +++ b/checks_ext/example/core/uri/host.dart @@ -0,0 +1,25 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final uri = Uri.parse('https://example.com'); + + /// This check succeeds. + check(uri).host.equals('example.com'); + + /// This check fails. + try { + check(uri).host.equals('google.com'); + } catch (e) { + print(e); + } + // Expected: a Uri that: + // has host that: + // equals 'google.com' + // Actual: a Uri that: + // has host that: + // Actual: 'example.com' + // Which: differs at offset 0: + // google.com ... + // example.co ... + // ^ +} diff --git a/checks_ext/example/core/uridata/is_base64.dart b/checks_ext/example/core/uridata/is_base64.dart new file mode 100644 index 00000000..f7ee3020 --- /dev/null +++ b/checks_ext/example/core/uridata/is_base64.dart @@ -0,0 +1,20 @@ +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final uriData = UriData.fromString('hello', base64: true); + + /// This check succeeds. + check(uriData).isBase64(); + + /// This check fails. + final rawData = UriData.fromString('hello', base64: false); + try { + check(rawData).isBase64(); + } catch (e) { + print(e); + } + // Expected: a UriData that: + // is base64 + // Actual: + // Which: is not base64 +} diff --git a/checks_ext/example/math/point/magnitude.dart b/checks_ext/example/math/point/magnitude.dart new file mode 100644 index 00000000..e351c56e --- /dev/null +++ b/checks_ext/example/math/point/magnitude.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final point = Point(3, 4); // magnitude is 5.0 + + /// This check succeeds. + check(point).magnitude.equals(5.0); + + /// This check fails. + try { + check(point).magnitude.equals(10.0); + } catch (e) { + print(e); + // Expected: a Point that: + // has magnitude that: + // equals <10.0> + // Actual: a Point that: + // has magnitude that: + // Actual: <5.0> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/point/x.dart b/checks_ext/example/math/point/x.dart new file mode 100644 index 00000000..679c2974 --- /dev/null +++ b/checks_ext/example/math/point/x.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final point = Point(3, 4); + + /// This check succeeds. + check(point).x.equals(3); + + /// This check fails. + try { + check(point).x.equals(10); + } catch (e) { + print(e); + // Expected: a Point that: + // has x that: + // equals <10> + // Actual: a Point that: + // has x that: + // Actual: <3> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/point/y.dart b/checks_ext/example/math/point/y.dart new file mode 100644 index 00000000..0d28405e --- /dev/null +++ b/checks_ext/example/math/point/y.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final point = Point(3, 4); + + /// This check succeeds. + check(point).y.equals(4); + + /// This check fails. + try { + check(point).y.equals(10); + } catch (e) { + print(e); + // Expected: a Point that: + // has y that: + // equals <10> + // Actual: a Point that: + // has y that: + // Actual: <4> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/rectangle/bottom.dart b/checks_ext/example/math/rectangle/bottom.dart new file mode 100644 index 00000000..7d65b83f --- /dev/null +++ b/checks_ext/example/math/rectangle/bottom.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).bottom.equals(60); + + /// This check fails. + try { + check(rect).bottom.equals(0); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // has bottom that: + // equals <0> + // Actual: a Rectangle that: + // has bottom that: + // Actual: <60> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/rectangle/contains_point.dart b/checks_ext/example/math/rectangle/contains_point.dart new file mode 100644 index 00000000..20906d16 --- /dev/null +++ b/checks_ext/example/math/rectangle/contains_point.dart @@ -0,0 +1,20 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).containsPoint(Point(15, 25)); + + /// This check fails. + try { + check(rect).containsPoint(Point(0, 0)); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // contains point + // Actual: + // Which: does not contain point + } +} diff --git a/checks_ext/example/math/rectangle/contains_rectangle.dart b/checks_ext/example/math/rectangle/contains_rectangle.dart new file mode 100644 index 00000000..7e4d53d1 --- /dev/null +++ b/checks_ext/example/math/rectangle/contains_rectangle.dart @@ -0,0 +1,20 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).containsRectangle(Rectangle(15, 25, 10, 10)); + + /// This check fails. + try { + check(rect).containsRectangle(Rectangle(0, 0, 5, 5)); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // contains rectangle + // Actual: + // Which: does not contain rectangle + } +} diff --git a/checks_ext/example/math/rectangle/height.dart b/checks_ext/example/math/rectangle/height.dart new file mode 100644 index 00000000..fc304bba --- /dev/null +++ b/checks_ext/example/math/rectangle/height.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).height.equals(40); + + /// This check fails. + try { + check(rect).height.equals(0); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // has height that: + // equals <0> + // Actual: a Rectangle that: + // has height that: + // Actual: <40> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/rectangle/left.dart b/checks_ext/example/math/rectangle/left.dart new file mode 100644 index 00000000..859e03f5 --- /dev/null +++ b/checks_ext/example/math/rectangle/left.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).left.equals(10); + + /// This check fails. + try { + check(rect).left.equals(0); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // has left that: + // equals <0> + // Actual: a Rectangle that: + // has left that: + // Actual: <10> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/rectangle/right.dart b/checks_ext/example/math/rectangle/right.dart new file mode 100644 index 00000000..f5d09e71 --- /dev/null +++ b/checks_ext/example/math/rectangle/right.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).right.equals(40); + + /// This check fails. + try { + check(rect).right.equals(0); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // has right that: + // equals <0> + // Actual: a Rectangle that: + // has right that: + // Actual: <40> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/rectangle/top.dart b/checks_ext/example/math/rectangle/top.dart new file mode 100644 index 00000000..f54cccac --- /dev/null +++ b/checks_ext/example/math/rectangle/top.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).top.equals(20); + + /// This check fails. + try { + check(rect).top.equals(0); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // has top that: + // equals <0> + // Actual: a Rectangle that: + // has top that: + // Actual: <20> + // Which: are not equal + } +} diff --git a/checks_ext/example/math/rectangle/width.dart b/checks_ext/example/math/rectangle/width.dart new file mode 100644 index 00000000..2067bb65 --- /dev/null +++ b/checks_ext/example/math/rectangle/width.dart @@ -0,0 +1,23 @@ +import 'dart:math'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final rect = Rectangle(10, 20, 30, 40); + + /// This check succeeds. + check(rect).width.equals(30); + + /// This check fails. + try { + check(rect).width.equals(0); + } catch (e) { + print(e); + // Expected: a Rectangle that: + // has width that: + // equals <0> + // Actual: a Rectangle that: + // has width that: + // Actual: <30> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_float32_list.dart b/checks_ext/example/typed_data/bytebuffer/as_float32_list.dart new file mode 100644 index 00000000..cd545b01 --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_float32_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Float32List.fromList([1.0, 2.0]).buffer; + + /// This check succeeds. + check(buffer).asFloat32List().deepEquals([1.0, 2.0]); + + /// This check fails. + try { + check(buffer).asFloat32List().deepEquals([0.0, 0.0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Float32List that: + // is deeply equal to [0.0, 0.0] + // Actual: a ByteBuffer that: + // as Float32List that: + // Actual: [1.0, 2.0] + // Which: at [<0>] is <1.0> + // which does not equal <0.0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_float64_list.dart b/checks_ext/example/typed_data/bytebuffer/as_float64_list.dart new file mode 100644 index 00000000..6e54ac3b --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_float64_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Float64List.fromList([1.0, 2.0]).buffer; + + /// This check succeeds. + check(buffer).asFloat64List().deepEquals([1.0, 2.0]); + + /// This check fails. + try { + check(buffer).asFloat64List().deepEquals([0.0, 0.0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Float64List that: + // is deeply equal to [0.0, 0.0] + // Actual: a ByteBuffer that: + // as Float64List that: + // Actual: [1.0, 2.0] + // Which: at [<0>] is <1.0> + // which does not equal <0.0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_int16_list.dart b/checks_ext/example/typed_data/bytebuffer/as_int16_list.dart new file mode 100644 index 00000000..62fe624e --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_int16_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Int16List.fromList([1, 2]).buffer; + + /// This check succeeds. + check(buffer).asInt16List().deepEquals([1, 2]); + + /// This check fails. + try { + check(buffer).asInt16List().deepEquals([0, 0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Int16List that: + // is deeply equal to [0, 0] + // Actual: a ByteBuffer that: + // as Int16List that: + // Actual: [1, 2] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_int32_list.dart b/checks_ext/example/typed_data/bytebuffer/as_int32_list.dart new file mode 100644 index 00000000..5250357c --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_int32_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Int32List.fromList([1, 2]).buffer; + + /// This check succeeds. + check(buffer).asInt32List().deepEquals([1, 2]); + + /// This check fails. + try { + check(buffer).asInt32List().deepEquals([0, 0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Int32List that: + // is deeply equal to [0, 0] + // Actual: a ByteBuffer that: + // as Int32List that: + // Actual: [1, 2] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_int8_list.dart b/checks_ext/example/typed_data/bytebuffer/as_int8_list.dart new file mode 100644 index 00000000..a395f3fe --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_int8_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Int8List.fromList([1, 2, 3, 4]).buffer; + + /// This check succeeds. + check(buffer).asInt8List().deepEquals([1, 2, 3, 4]); + + /// This check fails. + try { + check(buffer).asInt8List().deepEquals([0, 0, 0, 0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Int8List that: + // is deeply equal to [0, 0, 0, 0] + // Actual: a ByteBuffer that: + // as Int8List that: + // Actual: [1, 2, 3, 4] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_uint16_list.dart b/checks_ext/example/typed_data/bytebuffer/as_uint16_list.dart new file mode 100644 index 00000000..7ebd906c --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_uint16_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Uint16List.fromList([1, 2]).buffer; + + /// This check succeeds. + check(buffer).asUint16List().deepEquals([1, 2]); + + /// This check fails. + try { + check(buffer).asUint16List().deepEquals([0, 0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Uint16List that: + // is deeply equal to [0, 0] + // Actual: a ByteBuffer that: + // as Uint16List that: + // Actual: [1, 2] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_uint32_list.dart b/checks_ext/example/typed_data/bytebuffer/as_uint32_list.dart new file mode 100644 index 00000000..2fdbd084 --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_uint32_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Uint32List.fromList([1, 2]).buffer; + + /// This check succeeds. + check(buffer).asUint32List().deepEquals([1, 2]); + + /// This check fails. + try { + check(buffer).asUint32List().deepEquals([0, 0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Uint32List that: + // is deeply equal to [0, 0] + // Actual: a ByteBuffer that: + // as Uint32List that: + // Actual: [1, 2] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/as_uint8_list.dart b/checks_ext/example/typed_data/bytebuffer/as_uint8_list.dart new file mode 100644 index 00000000..03a074ce --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/as_uint8_list.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Uint8List.fromList([1, 2, 3, 4]).buffer; + + /// This check succeeds. + check(buffer).asUint8List().deepEquals([1, 2, 3, 4]); + + /// This check fails. + try { + check(buffer).asUint8List().deepEquals([0, 0, 0, 0]); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // as Uint8List that: + // is deeply equal to [0, 0, 0, 0] + // Actual: a ByteBuffer that: + // as Uint8List that: + // Actual: [1, 2, 3, 4] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytebuffer/length_in_bytes.dart b/checks_ext/example/typed_data/bytebuffer/length_in_bytes.dart new file mode 100644 index 00000000..781fedd5 --- /dev/null +++ b/checks_ext/example/typed_data/bytebuffer/length_in_bytes.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Uint8List(4).buffer; + + /// This check succeeds. + check(buffer).lengthInBytes.equals(4); + + /// This check fails. + try { + check(buffer).lengthInBytes.equals(10); + } catch (e) { + print(e); + // Expected: a ByteBuffer that: + // has lengthInBytes that: + // equals <10> + // Actual: a ByteBuffer that: + // has lengthInBytes that: + // Actual: <4> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/float32_at.dart b/checks_ext/example/typed_data/bytedata/float32_at.dart new file mode 100644 index 00000000..0875e0f5 --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/float32_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setFloat32(0, 3.14, Endian.little); + + /// This check succeeds. + check(data).float32At(0).isCloseTo(3.14, 0.01); + + /// This check fails. + try { + check(data).float32At(0).equals(0.0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has float32 at 0 that: + // equals <0.0> + // Actual: a ByteData that: + // has float32 at 0 that: + // Actual: <3.140000104904175> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/float64_at.dart b/checks_ext/example/typed_data/bytedata/float64_at.dart new file mode 100644 index 00000000..a8980fdf --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/float64_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(8)..setFloat64(0, 3.1415926535, Endian.little); + + /// This check succeeds. + check(data).float64At(0).equals(3.1415926535); + + /// This check fails. + try { + check(data).float64At(0).equals(0.0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has float64 at 0 that: + // equals <0.0> + // Actual: a ByteData that: + // has float64 at 0 that: + // Actual: <3.1415926535> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/int16_at.dart b/checks_ext/example/typed_data/bytedata/int16_at.dart new file mode 100644 index 00000000..4c36df6a --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/int16_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setInt16(0, -1000, Endian.little); + + /// This check succeeds. + check(data).int16At(0).equals(-1000); + + /// This check fails. + try { + check(data).int16At(0).equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has int16 at 0 that: + // equals <0> + // Actual: a ByteData that: + // has int16 at 0 that: + // Actual: <-1000> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/int32_at.dart b/checks_ext/example/typed_data/bytedata/int32_at.dart new file mode 100644 index 00000000..71b7575c --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/int32_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setInt32(0, -100000, Endian.little); + + /// This check succeeds. + check(data).int32At(0).equals(-100000); + + /// This check fails. + try { + check(data).int32At(0).equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has int32 at 0 that: + // equals <0> + // Actual: a ByteData that: + // has int32 at 0 that: + // Actual: <-100000> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/int8_at.dart b/checks_ext/example/typed_data/bytedata/int8_at.dart new file mode 100644 index 00000000..67eb8f45 --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/int8_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setInt8(0, -42); + + /// This check succeeds. + check(data).int8At(0).equals(-42); + + /// This check fails. + try { + check(data).int8At(0).equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has int8 at 0 that: + // equals <0> + // Actual: a ByteData that: + // has int8 at 0 that: + // Actual: <-42> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/length_in_bytes.dart b/checks_ext/example/typed_data/bytedata/length_in_bytes.dart new file mode 100644 index 00000000..0e51ee5b --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/length_in_bytes.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(8); + + /// This check succeeds. + check(data).lengthInBytes.equals(8); + + /// This check fails. + try { + check(data).lengthInBytes.equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has lengthInBytes that: + // equals <0> + // Actual: a ByteData that: + // has lengthInBytes that: + // Actual: <8> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/uint16_at.dart b/checks_ext/example/typed_data/bytedata/uint16_at.dart new file mode 100644 index 00000000..ca3ec06a --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/uint16_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setUint16(0, 1000, Endian.little); + + /// This check succeeds. + check(data).uint16At(0).equals(1000); + + /// This check fails. + try { + check(data).uint16At(0).equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has uint16 at 0 that: + // equals <0> + // Actual: a ByteData that: + // has uint16 at 0 that: + // Actual: <1000> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/uint32_at.dart b/checks_ext/example/typed_data/bytedata/uint32_at.dart new file mode 100644 index 00000000..163e3692 --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/uint32_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setUint32(0, 100000, Endian.little); + + /// This check succeeds. + check(data).uint32At(0).equals(100000); + + /// This check fails. + try { + check(data).uint32At(0).equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has uint32 at 0 that: + // equals <0> + // Actual: a ByteData that: + // has uint32 at 0 that: + // Actual: <100000> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytedata/uint8_at.dart b/checks_ext/example/typed_data/bytedata/uint8_at.dart new file mode 100644 index 00000000..0d05baa7 --- /dev/null +++ b/checks_ext/example/typed_data/bytedata/uint8_at.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = ByteData(4)..setUint8(0, 42); + + /// This check succeeds. + check(data).uint8At(0).equals(42); + + /// This check fails. + try { + check(data).uint8At(0).equals(0); + } catch (e) { + print(e); + // Expected: a ByteData that: + // has uint8 at 0 that: + // equals <0> + // Actual: a ByteData that: + // has uint8 at 0 that: + // Actual: <42> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/bytesbuilder/bytes.dart b/checks_ext/example/typed_data/bytesbuilder/bytes.dart new file mode 100644 index 00000000..745f361c --- /dev/null +++ b/checks_ext/example/typed_data/bytesbuilder/bytes.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final builder = BytesBuilder()..add([1, 2, 3]); + + /// This check succeeds. + check(builder).bytes.deepEquals([1, 2, 3]); + + /// This check fails. + try { + check(builder).bytes.deepEquals([0, 0, 0]); + } catch (e) { + print(e); + // Expected: a BytesBuilder that: + // has bytes that: + // is deeply equal to [0, 0, 0] + // Actual: a BytesBuilder that: + // has bytes that: + // Actual: [1, 2, 3] + // Which: at [<0>] is <1> + // which does not equal <0> + } +} diff --git a/checks_ext/example/typed_data/bytesbuilder/is_empty.dart b/checks_ext/example/typed_data/bytesbuilder/is_empty.dart new file mode 100644 index 00000000..d353d3d7 --- /dev/null +++ b/checks_ext/example/typed_data/bytesbuilder/is_empty.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final builder = BytesBuilder(); + + /// This check succeeds. + check(builder).isEmpty(); + + /// This check fails. + try { + check(BytesBuilder()..add([1])).isEmpty(); + } catch (e) { + print(e); + // Expected: a BytesBuilder that: + // is empty + // Actual: + // Which: is not empty + } +} diff --git a/checks_ext/example/typed_data/bytesbuilder/length.dart b/checks_ext/example/typed_data/bytesbuilder/length.dart new file mode 100644 index 00000000..f10eb2fe --- /dev/null +++ b/checks_ext/example/typed_data/bytesbuilder/length.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final builder = BytesBuilder()..add([1, 2, 3]); + + /// This check succeeds. + check(builder).length.equals(3); + + /// This check fails. + try { + check(builder).length.equals(0); + } catch (e) { + print(e); + // Expected: a BytesBuilder that: + // has length that: + // equals <0> + // Actual: a BytesBuilder that: + // has length that: + // Actual: <3> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/endian/is_big_endian.dart b/checks_ext/example/typed_data/endian/is_big_endian.dart new file mode 100644 index 00000000..b30883b1 --- /dev/null +++ b/checks_ext/example/typed_data/endian/is_big_endian.dart @@ -0,0 +1,18 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(Endian.big).isBigEndian(); + + /// This check fails. + try { + check(Endian.little).isBigEndian(); + } catch (e) { + print(e); + } + // Expected: a Endian that: + // is Endian.big + // Actual: + // Which: is Endian.little +} diff --git a/checks_ext/example/typed_data/endian/is_host.dart b/checks_ext/example/typed_data/endian/is_host.dart new file mode 100644 index 00000000..ace6b314 --- /dev/null +++ b/checks_ext/example/typed_data/endian/is_host.dart @@ -0,0 +1,21 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final endian = Endian.host; + + /// This check succeeds. + check(endian).isHost(); + + /// This check fails. + final nonHost = Endian.host == Endian.big ? Endian.little : Endian.big; + try { + check(nonHost).isHost(); + } catch (e) { + print(e); + } + // Expected: a Endian that: + // is Endian.host + // Actual: + // Which: is Endian.big +} diff --git a/checks_ext/example/typed_data/endian/is_little_endian.dart b/checks_ext/example/typed_data/endian/is_little_endian.dart new file mode 100644 index 00000000..8812b758 --- /dev/null +++ b/checks_ext/example/typed_data/endian/is_little_endian.dart @@ -0,0 +1,18 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + /// This check succeeds. + check(Endian.little).isLittleEndian(); + + /// This check fails. + try { + check(Endian.big).isLittleEndian(); + } catch (e) { + print(e); + } + // Expected: a Endian that: + // is Endian.little + // Actual: + // Which: is Endian.big +} diff --git a/checks_ext/example/typed_data/simd/float32x4.dart b/checks_ext/example/typed_data/simd/float32x4.dart new file mode 100644 index 00000000..6b3a004a --- /dev/null +++ b/checks_ext/example/typed_data/simd/float32x4.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final v = Float32x4(1.0, 2.0, 3.0, 4.0); + + /// This check succeeds. + check(v).x.equals(1.0); + check(v).y.equals(2.0); + check(v).z.equals(3.0); + check(v).w.equals(4.0); + + /// This check fails. + try { + check(v).x.equals(0.0); + } catch (e) { + print(e); + // Expected: a Float32x4 that: + // has x that: + // equals <0.0> + // Actual: a Float32x4 that: + // has x that: + // Actual: <1.0> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/simd/float64x2.dart b/checks_ext/example/typed_data/simd/float64x2.dart new file mode 100644 index 00000000..39d61245 --- /dev/null +++ b/checks_ext/example/typed_data/simd/float64x2.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final v = Float64x2(1.0, 2.0); + + /// This check succeeds. + check(v).x.equals(1.0); + check(v).y.equals(2.0); + + /// This check fails. + try { + check(v).x.equals(0.0); + } catch (e) { + print(e); + // Expected: a Float64x2 that: + // has x that: + // equals <0.0> + // Actual: a Float64x2 that: + // has x that: + // Actual: <1.0> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/simd/int32x4.dart b/checks_ext/example/typed_data/simd/int32x4.dart new file mode 100644 index 00000000..fc5fd41f --- /dev/null +++ b/checks_ext/example/typed_data/simd/int32x4.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final v = Int32x4(1, 2, 3, 4); + + /// This check succeeds. + check(v).x.equals(1); + check(v).y.equals(2); + check(v).z.equals(3); + check(v).w.equals(4); + + /// This check fails. + try { + check(v).x.equals(0); + } catch (e) { + print(e); + // Expected: a Int32x4 that: + // has x that: + // equals <0> + // Actual: a Int32x4 that: + // has x that: + // Actual: <1> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/typeddata/length_in_bytes.dart b/checks_ext/example/typed_data/typeddata/length_in_bytes.dart new file mode 100644 index 00000000..a561f2b4 --- /dev/null +++ b/checks_ext/example/typed_data/typeddata/length_in_bytes.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final data = Uint8List(8); + + /// This check succeeds. + check(data).lengthInBytes.equals(8); + + /// This check fails. + try { + check(data).lengthInBytes.equals(0); + } catch (e) { + print(e); + // Expected: a Uint8List that: + // has lengthInBytes that: + // equals <0> + // Actual: a Uint8List that: + // has lengthInBytes that: + // Actual: <8> + // Which: are not equal + } +} diff --git a/checks_ext/example/typed_data/typeddata/offset_in_bytes.dart b/checks_ext/example/typed_data/typeddata/offset_in_bytes.dart new file mode 100644 index 00000000..9ca80c08 --- /dev/null +++ b/checks_ext/example/typed_data/typeddata/offset_in_bytes.dart @@ -0,0 +1,24 @@ +import 'dart:typed_data'; +import 'package:checks_ext/checks_ext.dart'; + +void main() { + final buffer = Uint8List(8).buffer; + final data = Uint8List.view(buffer, 2); + + /// This check succeeds. + check(data).offsetInBytes.equals(2); + + /// This check fails. + try { + check(data).offsetInBytes.equals(0); + } catch (e) { + print(e); + // Expected: a Uint8List that: + // has offsetInBytes that: + // equals <0> + // Actual: a Uint8List that: + // has offsetInBytes that: + // Actual: <2> + // Which: are not equal + } +} diff --git a/checks_ext/lib/checks_ext.dart b/checks_ext/lib/checks_ext.dart new file mode 100644 index 00000000..0615c88a --- /dev/null +++ b/checks_ext/lib/checks_ext.dart @@ -0,0 +1,44 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'package:checks/checks.dart'; + +export 'src/async/completer.dart'; +export 'src/async/streamcontroller.dart'; +export 'src/core/bigint.dart'; +export 'src/core/comparable.dart'; +export 'src/core/datetime.dart'; +export 'src/core/duration.dart'; +export 'src/core/enum.dart'; +export 'src/core/iterable.dart'; +export 'src/core/list.dart'; +export 'src/core/mapentry.dart'; +export 'src/core/match.dart'; +export 'src/core/num.dart'; +export 'src/core/regexp.dart'; +export 'src/core/regexpmatch.dart'; +export 'src/core/set.dart'; +export 'src/core/stopwatch.dart'; +export 'src/core/string.dart'; +export 'src/core/stringbuffer.dart'; +export 'src/core/uri.dart'; +export 'src/core/uridata.dart'; +export 'src/math/point.dart'; +export 'src/math/rectangle.dart'; +export 'src/typed_data/bytebuffer.dart'; +export 'src/typed_data/bytedata.dart'; +export 'src/typed_data/bytesbuilder.dart'; +export 'src/typed_data/endian.dart'; +export 'src/typed_data/simd.dart'; +export 'src/typed_data/typeddata.dart'; diff --git a/checks_ext/lib/src/async/completer.dart b/checks_ext/lib/src/async/completer.dart new file mode 100644 index 00000000..47e3497a --- /dev/null +++ b/checks_ext/lib/src/async/completer.dart @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import '../util.dart'; + +extension CompleterChecksExt on Subject> { + static const _completed = ( + positive: 'is completed', + negative: 'is not completed', + ); + + /// Expects that the completer is completed. + /// + /// {@example /example/async/completer/is_completed.dart} + void isCompleted() => expectTrue(_completed, (s) => s.isCompleted); + + /// Expects that the completer is not completed. + void isNotCompleted() => expectFalse(_completed, (s) => s.isCompleted); + + /// The future that is completed by this completer. + Subject> get future => has((s) => s.future, 'future'); + + /// Expects that the future completes, optionally matching [completionCondition]. + Future completes([AsyncCondition? completionCondition]) => + future.completes(completionCondition); + + /// Expects that the future does not complete. + void doesNotComplete() => future.doesNotComplete(); + + /// Expects that the future throws an exception of type [E]. + Future throws([AsyncCondition? errorCondition]) => + future.throws(errorCondition); +} diff --git a/checks_ext/lib/src/async/streamcontroller.dart b/checks_ext/lib/src/async/streamcontroller.dart new file mode 100644 index 00000000..111fc490 --- /dev/null +++ b/checks_ext/lib/src/async/streamcontroller.dart @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import '../util.dart'; + +extension StreamControllerChecksExt on Subject> { + static const _hasListener = ( + positive: 'has listener', + negative: 'has no listener', + ); + + /// Expects that the stream controller has a listener. + /// + /// {@example /example/async/streamcontroller/has_listener.dart} + void hasListener() => expectTrue(_hasListener, (s) => s.hasListener); + + /// Expects that the stream controller has no listener. + void hasNoListener() => expectFalse(_hasListener, (s) => s.hasListener); + + static const _closed = (positive: 'is closed', negative: 'is not closed'); + + /// Expects that the stream controller is closed. + /// + /// {@example /example/async/streamcontroller/is_closed.dart} + void isClosed() => expectTrue(_closed, (s) => s.isClosed); + + /// Expects that the stream controller is not closed. + void isNotClosed() => expectFalse(_closed, (s) => s.isClosed); + + static const _paused = (positive: 'is paused', negative: 'is not paused'); + + /// Expects that the stream controller is paused. + /// + /// {@example /example/async/streamcontroller/is_paused.dart} + void isPaused() => expectTrue(_paused, (s) => s.isPaused); + + /// Expects that the stream controller is not paused. + void isNotPaused() => expectFalse(_paused, (s) => s.isPaused); + + /// The future that completes when the stream controller is closed. + Subject get done => has((s) => s.done, 'done'); +} diff --git a/checks_ext/lib/src/core/bigint.dart b/checks_ext/lib/src/core/bigint.dart new file mode 100644 index 00000000..ca0746ad --- /dev/null +++ b/checks_ext/lib/src/core/bigint.dart @@ -0,0 +1,77 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension BigIntChecksExt on Subject { + /// The bit length of this big integer. + /// + /// {@example /example/core/bigint/bit_length.dart} + Subject get bitLength => has((s) => s.bitLength, 'bitLength'); + + /// The sign of this big integer. + /// + /// {@example /example/core/bigint/sign.dart} + Subject get sign => has((s) => s.sign, 'sign'); + + static const _even = (positive: 'is even', negative: 'is not even'); + + /// Expects that this big integer is even. + /// + /// {@example /example/core/bigint/is_even.dart} + void isEven() => expectTrue(_even, (s) => s.isEven); + + /// Expects that this big integer is not even. + /// + /// {@example /example/core/bigint/is_not_even.dart} + void isNotEven() => expectFalse(_even, (s) => s.isEven); + + static const _odd = (positive: 'is odd', negative: 'is not odd'); + + /// Expects that this big integer is odd. + /// + /// {@example /example/core/bigint/is_odd.dart} + void isOdd() => expectTrue(_odd, (s) => s.isOdd); + + /// Expects that this big integer is not odd. + /// + /// {@example /example/core/bigint/is_not_odd.dart} + void isNotOdd() => expectFalse(_odd, (s) => s.isOdd); + + static const _negative = ( + positive: 'is negative', + negative: 'is not negative', + ); + + /// Expects that this big integer is negative. + /// + /// {@example /example/core/bigint/is_negative.dart} + void isNegative() => expectTrue(_negative, (s) => s.isNegative); + + /// Expects that this big integer is not negative. + void isNotNegative() => expectFalse(_negative, (s) => s.isNegative); + + static const _validInt = ( + positive: 'is valid int', + negative: 'is not valid int', + ); + + /// Expects that this big integer is a valid 64-bit integer. + /// + /// {@example /example/core/bigint/is_valid_int.dart} + void isValidInt() => expectTrue(_validInt, (s) => s.isValidInt); + + /// Expects that this big integer is not a valid 64-bit integer. + void isNotValidInt() => expectFalse(_validInt, (s) => s.isValidInt); +} diff --git a/checks_ext/lib/src/core/comparable.dart b/checks_ext/lib/src/core/comparable.dart new file mode 100644 index 00000000..0fbc9317 --- /dev/null +++ b/checks_ext/lib/src/core/comparable.dart @@ -0,0 +1,134 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; +import 'package:collection/collection.dart'; + +extension IterableComparableChecksExt> + on Subject> { + /// Expects that the iterable is sorted in natural order. + /// + /// {@example /example/core/comparable/is_sorted.dart} + void isSorted() => context.expect(() => ['is sorted'], (actual) { + final list = actual.toList(); + for (var i = 0; i < list.length - 1; i++) { + final a = list[i]; + final b = list[i + 1]; + if (a.compareTo(b) > 0) { + return Rejection( + which: [ + ...prefixFirst( + 'is not sorted, because element at index $i ', + literal(a), + ), + ...prefixFirst( + 'compares after element at index ${i + 1} ', + literal(b), + ), + ], + ); + } + } + return null; + }); + + /// The maximum element in the iterable. + /// + /// {@example /example/core/comparable/max.dart} + Subject get max => context.nest(() => ['has max'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.max); + }); + + /// The minimum element in the iterable. + /// + /// {@example /example/core/comparable/min.dart} + Subject get min => context.nest(() => ['has min'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.min); + }); +} + +extension IterableDoubleChecksExt on Subject> { + /// The maximum element in the iterable. + Subject get max => context.nest(() => ['has max'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.max); + }); + + /// The minimum element in the iterable. + Subject get min => context.nest(() => ['has min'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.min); + }); + + /// The sum of the elements in the iterable. + Subject get sum => context.nest(() => ['has sum'], (actual) { + return Extracted.value(actual.sum); + }); +} + +extension IterableIntegerChecksExt on Subject> { + /// The maximum element in the iterable. + Subject get max => context.nest(() => ['has max'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.max); + }); + + /// The minimum element in the iterable. + Subject get min => context.nest(() => ['has min'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.min); + }); + + /// The sum of the elements in the iterable. + /// + /// {@example /example/core/comparable/sum.dart} + Subject get sum => context.nest(() => ['has sum'], (actual) { + return Extracted.value(actual.sum); + }); + + /// The average of the elements in the iterable. + /// + /// {@example /example/core/comparable/average.dart} + Subject get average => context.nest(() => ['has average'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.average); + }); +} + +extension IterableNumberChecksExt on Subject> { + /// The maximum element in the iterable. + Subject get max => context.nest(() => ['has max'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.max); + }); + + /// The minimum element in the iterable. + Subject get min => context.nest(() => ['has min'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.min); + }); + + /// The sum of the elements in the iterable. + Subject get sum => context.nest(() => ['has sum'], (actual) { + return Extracted.value(actual.sum); + }); + + /// The average of the elements in the iterable. + Subject get average => context.nest(() => ['has average'], (actual) { + if (actual.isEmpty) return Extracted.rejection(which: ['is empty']); + return Extracted.value(actual.average); + }); +} diff --git a/checks_ext/lib/src/core/datetime.dart b/checks_ext/lib/src/core/datetime.dart new file mode 100644 index 00000000..ad8f2e7c --- /dev/null +++ b/checks_ext/lib/src/core/datetime.dart @@ -0,0 +1,177 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension DateTimeChecksExt on Subject { + /// Expects that this date is after [other]. + /// + /// {@example /example/core/datetime/is_after.dart} + void isAfter(DateTime other) => + context.expect(() => prefixFirst('is after ', literal(other)), (actual) { + if (!actual.isAfter(other)) { + return Rejection( + which: [...prefixFirst('was not after ', literal(other))], + ); + } + return null; + }); + + /// Expects that this date is before [other]. + /// + /// {@example /example/core/datetime/is_before.dart} + void isBefore(DateTime other) => + context.expect(() => prefixFirst('is before ', literal(other)), (actual) { + if (!actual.isBefore(other)) { + return Rejection( + which: [...prefixFirst('was not before ', literal(other))], + ); + } + return null; + }); + + /// Expects that this date is at the same moment as [other]. + /// + /// {@example /example/core/datetime/is_at_same_moment_as.dart} + void isAtSameMomentAs(DateTime other) => context.expect( + () => prefixFirst('is at same moment as ', literal(other)), + (actual) { + if (!actual.isAtSameMomentAs(other)) { + return Rejection( + which: [...prefixFirst('was not at same moment as ', literal(other))], + ); + } + return null; + }, + ); + + /// Expects that this date is in UTC. + /// + /// {@example /example/core/datetime/is_utc.dart} + void isUtc() => expectTrue(_utc, (d) => d.isUtc); + + /// Expects that this date is not in UTC. + /// + /// {@example /example/core/datetime/is_not_utc.dart} + void isNotUtc() => expectFalse(_utc, (d) => d.isUtc); + + static const _utc = (positive: 'is UTC', negative: 'is not in UTC'); + + /// The year component of this date. + /// + /// {@example /example/core/datetime/year.dart} + Subject get year => has((d) => d.year, 'year'); + + /// The month component of this date. + /// + /// {@example /example/core/datetime/month.dart} + Subject get month => has((d) => d.month, 'month'); + + /// The day component of this date. + /// + /// {@example /example/core/datetime/day.dart} + Subject get day => has((d) => d.day, 'day'); + + /// The weekday component of this date. + /// + /// {@example /example/core/datetime/weekday.dart} + Subject get weekday => has((d) => d.weekday, 'weekday'); + + /// The hour component of this date. + /// + /// {@example /example/core/datetime/hour.dart} + Subject get hour => has((d) => d.hour, 'hour'); + + /// The minute component of this date. + /// + /// {@example /example/core/datetime/minute.dart} + Subject get minute => has((d) => d.minute, 'minute'); + + /// The second component of this date. + /// + /// {@example /example/core/datetime/second.dart} + Subject get second => has((d) => d.second, 'second'); + + /// The microseconds since epoch of this date. + /// + /// {@example /example/core/datetime/microseconds_since_epoch.dart} + Subject get microsecondsSinceEpoch => + has((d) => d.microsecondsSinceEpoch, 'microsecondsSinceEpoch'); + + /// Extracts the difference between this date and [other]. + /// + /// {@example /example/core/datetime/difference.dart} + Subject difference(DateTime other) => context.nest( + () => prefixFirst('difference from ', literal(other)), + (d) => Extracted.value(d.difference(other)), + ); + + /// Expects that this date is close to [other] within [within]. + /// + /// {@example /example/core/datetime/is_close_to.dart} + void isCloseTo(DateTime other, {required Duration within}) => context.expect( + () => prefixFirst( + 'is close to ', + postfixLast(' within ${prettyPrintDuration(within)}', literal(other)), + ), + (actual) { + final diff = actual.difference(other).abs(); + if (diff > within) { + return Rejection( + which: [ + 'differs by ${prettyPrintDuration(diff)}', + 'which is more than ${prettyPrintDuration(within)}', + ], + ); + } + return null; + }, + ); + + /// Expects that this date is between [start] and [end]. + /// + /// {@example /example/core/datetime/is_between.dart} + void isBetween(DateTime start, DateTime end, {bool inclusive = true}) => + context.expect( + () => [ + ...prefixFirst('is between ', literal(start)), + ...prefixFirst('and ', literal(end)), + inclusive ? ' (inclusive)' : ' (exclusive)', + ], + (actual) { + if (inclusive) { + if (actual.isBefore(start) || actual.isAfter(end)) { + return Rejection( + which: [ + ...prefixFirst('was not between ', literal(start)), + ...prefixFirst('and ', literal(end)), + ' (inclusive)', + ], + ); + } + } else { + if (!actual.isAfter(start) || !actual.isBefore(end)) { + return Rejection( + which: [ + ...prefixFirst('was not between ', literal(start)), + ...prefixFirst('and ', literal(end)), + ' (exclusive)', + ], + ); + } + } + return null; + }, + ); +} diff --git a/checks_ext/lib/src/core/duration.dart b/checks_ext/lib/src/core/duration.dart new file mode 100644 index 00000000..6cacd98a --- /dev/null +++ b/checks_ext/lib/src/core/duration.dart @@ -0,0 +1,157 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension DurationChecksExt on Subject { + /// Expects that this duration is greater than [other]. + /// + /// {@example /example/core/duration/is_greater_than.dart} + void isGreaterThan(Duration other) => context.expect( + () => ['is greater than ${prettyPrintDuration(other)}'], + (actual) { + if (actual <= other) { + return Rejection( + which: [ + 'was not greater than ${prettyPrintDuration(other)}', + 'actual was ${prettyPrintDuration(actual)}', + ], + ); + } + return null; + }, + ); + + /// Expects that this duration is less than [other]. + /// + /// {@example /example/core/duration/is_less_than.dart} + void isLessThan(Duration other) => context.expect( + () => ['is less than ${prettyPrintDuration(other)}'], + (actual) { + if (actual >= other) { + return Rejection( + which: [ + 'was not less than ${prettyPrintDuration(other)}', + 'actual was ${prettyPrintDuration(actual)}', + ], + ); + } + return null; + }, + ); + + /// Expects that this duration is greater than or equal to [other]. + void isGreaterOrEqual(Duration other) => context.expect( + () => ['is greater than or equal to ${prettyPrintDuration(other)}'], + (actual) { + if (actual < other) { + return Rejection( + which: [ + 'was not greater than or equal to ${prettyPrintDuration(other)}', + 'actual was ${prettyPrintDuration(actual)}', + ], + ); + } + return null; + }, + ); + + /// Expects that this duration is less than or equal to [other]. + void isLessOrEqual(Duration other) => context.expect( + () => ['is less than or equal to ${prettyPrintDuration(other)}'], + (actual) { + if (actual > other) { + return Rejection( + which: [ + 'was not less than or equal to ${prettyPrintDuration(other)}', + 'actual was ${prettyPrintDuration(actual)}', + ], + ); + } + return null; + }, + ); + + /// Expects that this duration is close to [other] within [within]. + /// + /// {@example /example/core/duration/is_close_to.dart} + void isCloseTo(Duration other, {required Duration within}) => context.expect( + () => [ + 'is close to ${prettyPrintDuration(other)} within ${prettyPrintDuration(within)}', + ], + (actual) { + final diff = (actual - other).abs(); + if (diff > within) { + return Rejection( + which: [ + 'differs by ${prettyPrintDuration(diff)}', + 'which is more than ${prettyPrintDuration(within)}', + ], + ); + } + return null; + }, + ); + + static const _negative = ( + positive: 'is negative', + negative: 'is not negative', + ); + + /// Expects that this duration is negative. + /// + /// {@example /example/core/duration/is_negative.dart} + void isNegative() => expectTrue(_negative, (d) => d.isNegative); + + /// Expects that this duration is not negative. + void isNotNegative() => expectFalse(_negative, (d) => d.isNegative); + + static const _zero = (positive: 'is zero', negative: 'is not zero'); + + /// Expects that this duration is zero. + /// + /// {@example /example/core/duration/is_zero.dart} + void isZero() => expectTrue(_zero, (d) => d == Duration.zero); + + /// Expects that this duration is not zero. + void isNotZero() => expectFalse(_zero, (d) => d == Duration.zero); + + /// The number of whole days spanned by this duration. + Subject get inDays => + context.nest(() => ['in days'], (d) => Extracted.value(d.inDays)); + + /// The number of whole hours spanned by this duration. + Subject get inHours => + context.nest(() => ['in hours'], (d) => Extracted.value(d.inHours)); + + /// The number of whole minutes spanned by this duration. + Subject get inMinutes => + context.nest(() => ['in minutes'], (d) => Extracted.value(d.inMinutes)); + + /// The number of whole seconds spanned by this duration. + Subject get inSeconds => + context.nest(() => ['in seconds'], (d) => Extracted.value(d.inSeconds)); + + /// The number of whole milliseconds spanned by this duration. + Subject get inMilliseconds => context.nest( + () => ['in milliseconds'], + (d) => Extracted.value(d.inMilliseconds), + ); + + /// The number of whole microseconds spanned by this duration. + Subject get inMicroseconds => context.nest( + () => ['in microseconds'], + (d) => Extracted.value(d.inMicroseconds), + ); +} diff --git a/checks_ext/lib/src/core/enum.dart b/checks_ext/lib/src/core/enum.dart new file mode 100644 index 00000000..6e9801d6 --- /dev/null +++ b/checks_ext/lib/src/core/enum.dart @@ -0,0 +1,25 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension EnumChecksExt on Subject { + /// Extracts the zero-based position of the constant in the enum declaration. + Subject get index => has((e) => e.index, 'index'); + + /// Extracts the name of the enum constant. + /// + /// {@example /example/core/enum/name.dart} + Subject get name => has((e) => e.name, 'name'); +} diff --git a/checks_ext/lib/src/core/iterable.dart b/checks_ext/lib/src/core/iterable.dart new file mode 100644 index 00000000..63ac2728 --- /dev/null +++ b/checks_ext/lib/src/core/iterable.dart @@ -0,0 +1,164 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; +import 'package:collection/collection.dart'; +import '../util.dart'; + +extension NullableIterableChecksExt on Subject> { + /// Returns a subject containing only the non-null elements. + /// + /// {@example /example/core/iterable/non_nulls.dart} + Subject> get nonNulls => context.nest( + () => ['skipping nulls'], + (value) => Extracted.value(value.nonNulls), + ); +} + +extension IterableChecksExt on Subject> { + /// Expects that the iterable is sorted by the key extracted by [keyOf]. + /// + /// {@example /example/core/iterable/is_sorted_by.dart} + void isSortedBy>( + K Function(T) keyOf, [ + String? keyName, + ]) => context.expect( + () => [ + if (keyName != null) + 'is sorted by key "$keyName"' + else + 'is sorted by key', + ], + (actual) { + final list = actual.toList(); + for (var i = 0; i < list.length - 1; i++) { + final a = keyOf(list[i]); + final b = keyOf(list[i + 1]); + if (a.compareTo(b) > 0) { + final keyPrefix = keyName != null + ? 'with key "$keyName" ' + : 'with key '; + return Rejection( + which: [ + ...prefixFirst( + 'is not sorted, because element at index $i ', + literal(list[i]), + ), + ...prefixFirst(keyPrefix, literal(a)), + ...prefixFirst( + 'compares after element at index ${i + 1} ', + literal(list[i + 1]), + ), + ...prefixFirst(keyPrefix, literal(b)), + ], + ); + } + } + return null; + }, + ); + + /// Expects that this iterable contains all elements of [expected]. + /// + /// {@example /example/core/iterable/contains_all.dart} + void containsAll(Iterable expected) { + context.expect(() => prefixFirst('contains all of ', literal(expected)), ( + actual, + ) { + final actualSet = actual.toSet(); + final missing = expected.where((e) => !actualSet.contains(e)).toList(); + + if (missing.isEmpty) return null; + + return Rejection( + which: [...prefixFirst('is missing elements: ', literal(missing))], + ); + }); + } + + /// Expects that this iterable contains the [expected] sequence contiguously. + /// + /// {@example /example/core/iterable/contains_sequence.dart} + void containsSequence(Iterable expected) { + context.expect(() => prefixFirst('contains sequence ', literal(expected)), ( + actual, + ) { + final actualList = actual.toList(); + final expectedList = expected.toList(); + + if (expectedList.isEmpty) return null; + + for (var i = 0; i <= actualList.length - expectedList.length; i++) { + var match = true; + for (var j = 0; j < expectedList.length; j++) { + if (actualList[i + j] != expectedList[j]) { + match = false; + break; + } + } + if (match) return null; + } + + return Rejection(which: ['does not contain the sequence']); + }); + } + + /// Expects that this iterable contains duplicate elements. + /// + /// {@example /example/core/iterable/has_duplicates.dart} + void hasDuplicates({Equality? equality}) { + context.expect(() => ['has duplicates'], (actual) { + final seen = HashSet( + equals: equality?.equals, + hashCode: equality?.hash, + ); + for (final element in actual) { + if (!seen.add(element)) { + return null; // Found a duplicate + } + } + return Rejection(which: ['has no duplicates']); + }); + } + + /// Expects that this iterable contains no duplicate elements. + /// + /// {@example /example/core/iterable/has_no_duplicates.dart} + void hasNoDuplicates({Equality? equality}) { + context.expect(() => ['has no duplicates'], (actual) { + final seen = HashSet( + equals: equality?.equals, + hashCode: equality?.hash, + ); + final duplicates = LinkedHashSet( + equals: equality?.equals, + hashCode: equality?.hash, + ); + + for (final element in actual) { + if (!seen.add(element)) { + duplicates.add(element); + } + } + + if (duplicates.isEmpty) return null; + + return Rejection( + which: [ + ...prefixFirst('has duplicates: ', literal(duplicates.toList())), + ], + ); + }); + } +} diff --git a/checks_ext/lib/src/core/list.dart b/checks_ext/lib/src/core/list.dart new file mode 100644 index 00000000..f34deb5c --- /dev/null +++ b/checks_ext/lib/src/core/list.dart @@ -0,0 +1,168 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:math'; +import 'package:collection/collection.dart'; +import '../util.dart'; + +extension ListIntChecksExt on Subject> { + /// Decodes the bytes as a UTF-8 string. + /// + /// {@example /example/core/list/as_utf8.dart} + Subject get asUtf8 => + context.nest(() => ['as UTF-8 string'], (bytes) { + try { + return Extracted.value(utf8.decode(bytes)); + } catch (e) { + return Extracted.rejection(which: ['is not valid UTF-8']); + } + }); + + /// Expects that this byte list is valid UTF-8. + /// + /// {@example /example/core/list/is_valid_utf8.dart} + void isValidUtf8() { + context.expect(() => ['is valid UTF-8'], (actual) { + try { + utf8.decode(actual); + return null; + } catch (e) { + return Rejection(which: ['is not valid UTF-8']); + } + }); + } + + /// Expects that this byte list contains the given [sequence] of bytes. + /// + /// {@example /example/core/list/contains_bytes.dart} + void containsBytes(List sequence) { + context.expect(() => prefixFirst('contains bytes ', literal(sequence)), ( + actual, + ) { + if (sequence.isEmpty) return null; + + for (var i = 0; i <= actual.length - sequence.length; i++) { + var match = true; + for (var j = 0; j < sequence.length; j++) { + if (actual[i + j] != sequence[j]) { + match = false; + break; + } + } + if (match) return null; + } + + bool isPrintable(int byte) => byte >= 32 && byte <= 126; + final allPrintable = + actual.every(isPrintable) && sequence.every(isPrintable); + + String toDisplay(List bytes) { + return bytes + .map((byte) { + return allPrintable + ? String.fromCharCode(byte) + : byte.toRadixString(16).padLeft(2, '0'); + }) + .join(' '); + } + + final actualDisplay = toDisplay(actual); + final sequenceDisplay = toDisplay(sequence); + + return Rejection( + which: [ + 'does not contain the sequence: $sequenceDisplay', + 'Actual bytes: $actualDisplay', + ], + ); + }); + } + + /// Expects that this byte list equals [expected] bytes, showing diff in hex. + /// + /// {@example /example/core/list/equals_bytes.dart} + void equalsBytes(List expected) { + context.expect(() => prefixFirst('equals ', literal(expected)), (actual) { + if (const ListEquality().equals(actual, expected)) return null; + + int? diffIndex; + final minLen = min(actual.length, expected.length); + for (var i = 0; i < minLen; i++) { + if (actual[i] != expected[i]) { + diffIndex = i; + break; + } + } + + diffIndex ??= minLen; + + final start = max(0, diffIndex - 5); + final endActual = min(actual.length, diffIndex + 5); + final endExpected = min(expected.length, diffIndex + 5); + + final actualSnippet = actual.sublist(start, endActual); + final expectedSnippet = expected.sublist(start, endExpected); + + bool isPrintable(int byte) => byte >= 32 && byte <= 126; + final allPrintable = + actualSnippet.every(isPrintable) && + expectedSnippet.every(isPrintable); + + String toDisplay( + List bytes, + int markIndex, + int startOffset, + int totalLength, + ) { + final parts = []; + if (startOffset > 0) parts.add('...'); + for (var i = 0; i < bytes.length; i++) { + final byte = bytes[i]; + final representation = allPrintable + ? String.fromCharCode(byte) + : byte.toRadixString(16).padLeft(2, '0'); + if (startOffset + i == markIndex) { + parts.add('[$representation]'); + } else { + parts.add(representation); + } + } + if (startOffset + bytes.length < totalLength) parts.add('...'); + return parts.join(' '); + } + + final actualDisplay = toDisplay( + actualSnippet, + diffIndex, + start, + actual.length, + ); + final expectedDisplay = toDisplay( + expectedSnippet, + diffIndex, + start, + expected.length, + ); + + return Rejection( + which: [ + 'differs at offset $diffIndex:', + 'Actual: $actualDisplay', + 'Expected: $expectedDisplay', + ], + ); + }); + } +} diff --git a/checks_ext/lib/src/core/map.dart b/checks_ext/lib/src/core/map.dart new file mode 100644 index 00000000..8025bf4e --- /dev/null +++ b/checks_ext/lib/src/core/map.dart @@ -0,0 +1,13 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/checks_ext/lib/src/core/mapentry.dart b/checks_ext/lib/src/core/mapentry.dart new file mode 100644 index 00000000..f0a36e38 --- /dev/null +++ b/checks_ext/lib/src/core/mapentry.dart @@ -0,0 +1,25 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks/checks.dart'; + +extension MapEntryChecksExt on Subject> { + /// + /// {@example /example/core/mapentry/key.dart} + Subject get key => has((e) => e.key, 'key'); + + /// + /// {@example /example/core/mapentry/value.dart} + Subject get value => has((e) => e.value, 'value'); +} diff --git a/checks_ext/lib/src/core/match.dart b/checks_ext/lib/src/core/match.dart new file mode 100644 index 00000000..03009344 --- /dev/null +++ b/checks_ext/lib/src/core/match.dart @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension MatchChecksExt on Subject { + /// The index in the string where the match starts. + /// + /// {@example /example/core/match/start.dart} + Subject get start => has((m) => m.start, 'start'); + + /// The index in the string after the last character of the match. + Subject get end => has((m) => m.end, 'end'); + + /// Returns the number of captured groups in the match. + Subject get groupCount => has((m) => m.groupCount, 'groupCount'); + + /// The string on which this match was computed. + Subject get input => has((m) => m.input, 'input'); + + /// Extracts the string matched by the given [group] index. + /// + /// {@example /example/core/match/group.dart} + Subject group(int index) => context.nest( + () => ['has group($index)'], + (m) => Extracted.value(m.group(index)), + ); + + /// The string matched by the given [group] index. + Subject operator [](int index) => group(index); +} diff --git a/checks_ext/lib/src/core/num.dart b/checks_ext/lib/src/core/num.dart new file mode 100644 index 00000000..a47137cd --- /dev/null +++ b/checks_ext/lib/src/core/num.dart @@ -0,0 +1,121 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension NumChecksExt on Subject { + /// Expects that the number is strictly greater than 0. + /// + /// {@example /example/core/num/is_strictly_positive.dart} + void isStrictlyPositive() { + context.expect(() => ['is strictly positive'], (actual) { + if (actual <= 0) { + return Rejection(which: ['is not strictly positive']); + } + return null; + }); + } + + /// Expects that the number is strictly less than 0. + void isStrictlyNegative() { + context.expect(() => ['is strictly negative'], (actual) { + if (actual >= 0) { + return Rejection(which: ['is not strictly negative']); + } + return null; + }); + } + + /// Expects that the number is between [min] and [max]. + /// + /// {@example /example/core/num/is_between.dart} + void isBetween(num min, num max, {bool inclusive = true}) { + context.expect( + () => [ + ...prefixFirst('is between ', literal(min)), + ...prefixFirst('and ', literal(max)), + inclusive ? ' (inclusive)' : ' (exclusive)', + ], + (actual) { + if (inclusive) { + if (actual < min || actual > max) { + return Rejection( + which: [ + ...prefixFirst('is not between ', literal(min)), + ...prefixFirst('and ', literal(max)), + ' (inclusive)', + ], + ); + } + } else { + if (actual <= min || actual >= max) { + return Rejection( + which: [ + ...prefixFirst('is not between ', literal(min)), + ...prefixFirst('and ', literal(max)), + ' (exclusive)', + ], + ); + } + } + return null; + }, + ); + } +} + +extension IntChecksExt on Subject { + /// Expects that the integer is even. + /// + /// {@example /example/core/num/is_even.dart} + void isEven() { + context.expect(() => ['is even'], (actual) { + if (actual % 2 != 0) { + return Rejection(which: ['is odd']); + } + return null; + }); + } + + /// Expects that the integer is odd. + void isOdd() { + context.expect(() => ['is odd'], (actual) { + if (actual % 2 == 0) { + return Rejection(which: ['is even']); + } + return null; + }); + } + + /// Expects that the integer is a multiple of [factor]. + /// + /// {@example /example/core/num/is_multiple_of.dart} + void isMultipleOf(int factor) { + context.expect(() => prefixFirst('is multiple of ', literal(factor)), ( + actual, + ) { + if (actual % factor != 0) { + return Rejection( + which: [...prefixFirst('is not a multiple of ', literal(factor))], + ); + } + return null; + }); + } + + /// Expects that the integer is divisible by [factor]. + /// + /// {@example /example/core/num/is_multiple_of.dart} + void isDivisibleBy(int factor) => isMultipleOf(factor); +} diff --git a/checks_ext/lib/src/core/regexp.dart b/checks_ext/lib/src/core/regexp.dart new file mode 100644 index 00000000..4830eee6 --- /dev/null +++ b/checks_ext/lib/src/core/regexp.dart @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension RegExpChecksExt on Subject { + /// The source pattern string of this regular expression. + /// + /// {@example /example/core/regexp/pattern.dart} + Subject get pattern => has((r) => r.pattern, 'pattern'); + + /// Whether this regular expression matches case-insensitively. + Subject get isCaseSensitive => + has((r) => r.isCaseSensitive, 'isCaseSensitive'); + + /// Whether this regular expression matches multiple lines. + Subject get isMultiLine => has((r) => r.isMultiLine, 'isMultiLine'); + + /// Whether "." matches line terminators. + Subject get isDotAll => has((r) => r.isDotAll, 'isDotAll'); + + /// Whether this regular expression uses Unicode mode. + Subject get isUnicode => has((r) => r.isUnicode, 'isUnicode'); + + /// Extracts the first match of this regular expression in [input]. + /// + /// {@example /example/core/regexp/first_match.dart} + Subject firstMatch(String input) => context.nest( + () => prefixFirst('has firstMatch on ', literal(input)), + (r) => Extracted.value(r.firstMatch(input)), + ); + + /// Extracts all matches of this regular expression in [input]. + /// + /// {@example /example/core/regexp/all_matches.dart} + Subject> allMatches(String input) => context.nest( + () => prefixFirst('has allMatches on ', literal(input)), + (r) => Extracted.value(r.allMatches(input)), + ); + + /// Expects that this regular expression has at least one match in [input]. + /// + /// {@example /example/core/regexp/has_match.dart} + void hasMatch(String input) { + context.expect(() => prefixFirst('matches ', literal(input)), (actual) { + if (actual.hasMatch(input)) return null; + return Rejection( + which: [...prefixFirst('does not match ', literal(input))], + ); + }); + } + + /// Expects that this regular expression has no matches in [input]. + /// + /// {@example /example/core/regexp/has_no_match.dart} + void hasNoMatch(String input) { + context.expect(() => prefixFirst('does not match ', literal(input)), ( + actual, + ) { + final match = actual.firstMatch(input); + if (match == null) return null; + + final start = match.start; + var lineStart = input.lastIndexOf('\n', start); + if (lineStart == -1) { + lineStart = 0; + } else { + lineStart += 1; + } + + var lineEnd = input.indexOf('\n', start); + if (lineEnd == -1) { + lineEnd = input.length; + } + + final line = input.substring(lineStart, lineEnd); + final offset = start - lineStart; + final spaces = ' ' * offset; + + return Rejection( + which: [ + ...prefixFirst( + 'matched ', + postfixLast(' at index $start:', literal(match[0])), + ), + line, + '$spaces^', + ], + ); + }); + } +} diff --git a/checks_ext/lib/src/core/regexpmatch.dart b/checks_ext/lib/src/core/regexpmatch.dart new file mode 100644 index 00000000..a8cc8954 --- /dev/null +++ b/checks_ext/lib/src/core/regexpmatch.dart @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension RegExpMatchChecksExt on Subject { + /// Extracts the string matched by the named group [name]. + /// + /// {@example /example/core/regexpmatch/named_group.dart} + Subject namedGroup(String name) => context.nest( + () => ['has namedGroup($name)'], + (m) => Extracted.value(m.namedGroup(name)), + ); + + /// The names of the named groups in the match. + Subject> get groupNames => + has((m) => m.groupNames, 'groupNames'); +} diff --git a/checks_ext/lib/src/core/set.dart b/checks_ext/lib/src/core/set.dart new file mode 100644 index 00000000..524a6bbc --- /dev/null +++ b/checks_ext/lib/src/core/set.dart @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension SetChecksExt on Subject> { + /// Expects that this set is a subset of [other]. + /// + /// {@example /example/core/set/is_subset_of.dart} + void isSubsetOf(Set other) { + context.expect(() => prefixFirst('is a subset of ', literal(other)), ( + actual, + ) { + final missing = actual.difference(other); + if (missing.isEmpty) return null; + return Rejection( + which: [ + ...prefixFirst( + 'contains elements not in the other set: ', + literal(missing), + ), + ], + ); + }); + } + + /// Expects that this set is a superset of [other]. + void isSupersetOf(Set other) { + context.expect(() => prefixFirst('is a superset of ', literal(other)), ( + actual, + ) { + final missing = other.difference(actual); + if (missing.isEmpty) return null; + return Rejection( + which: [ + ...prefixFirst( + 'is missing elements from the other set: ', + literal(missing), + ), + ], + ); + }); + } +} diff --git a/checks_ext/lib/src/core/stopwatch.dart b/checks_ext/lib/src/core/stopwatch.dart new file mode 100644 index 00000000..02f6fec0 --- /dev/null +++ b/checks_ext/lib/src/core/stopwatch.dart @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension StopwatchChecksExt on Subject { + /// The elapsed time measured by this stopwatch. + Subject get elapsed => has((s) => s.elapsed, 'elapsed'); + + /// Expects that the stopwatch is currently running. + void isRunning() { + context.expect(() => ['is running'], (actual) { + if (actual.isRunning) return null; + return Rejection( + actual: [ + 'Stopwatch(running: false, elapsed: ${prettyPrintDuration(actual.elapsed)})', + ], + which: ['is not running'], + ); + }); + } + + /// Expects that the stopwatch is not currently running. + void isNotRunning() { + context.expect(() => ['is not running'], (actual) { + if (!actual.isRunning) return null; + return Rejection( + actual: [ + 'Stopwatch(running: true, elapsed: ${prettyPrintDuration(actual.elapsed)})', + ], + which: ['is running'], + ); + }); + } + + /// The elapsed time measured by this stopwatch in ticks. + Subject get elapsedTicks => has((s) => s.elapsedTicks, 'elapsedTicks'); + + /// The elapsed time measured by this stopwatch in milliseconds. + Subject get elapsedMilliseconds => + has((s) => s.elapsedMilliseconds, 'elapsedMilliseconds'); +} diff --git a/checks_ext/lib/src/core/string.dart b/checks_ext/lib/src/core/string.dart new file mode 100644 index 00000000..ed0c12f4 --- /dev/null +++ b/checks_ext/lib/src/core/string.dart @@ -0,0 +1,150 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import '../util.dart'; + +extension StringChecksExt on Subject { + /// Expects that the string is empty or contains only whitespace. + /// + /// {@example /example/core/string/is_blank.dart} + void isBlank() { + context.expect(() => ['is blank'], (actual) { + if (actual.trim().isNotEmpty) { + return Rejection(which: ['was not blank']); + } + return null; + }); + } + + /// Expects that the string is not empty and contains at least one non-whitespace character. + /// + /// {@example /example/core/string/is_not_blank.dart} + void isNotBlank() { + context.expect(() => ['is not blank'], (actual) { + if (actual.trim().isEmpty) { + return Rejection(which: ['was blank']); + } + return null; + }); + } + + /// Extracts the lines of the string. + /// + /// {@example /example/core/string/lines.dart} + Subject> get lines => context.nest( + () => ['has lines'], + (actual) => Extracted.value(const LineSplitter().convert(actual)), + ); + + /// Expects that all alphabetic characters in the string are lowercase. + /// + /// {@example /example/core/string/is_lower_case.dart} + void isLowerCase() { + context.expect(() => ['is lower case'], (actual) { + if (actual != actual.toLowerCase()) { + return Rejection(which: ['was not lower case']); + } + return null; + }); + } + + /// Expects that all alphabetic characters in the string are uppercase. + /// + /// {@example /example/core/string/is_upper_case.dart} + void isUpperCase() { + context.expect(() => ['is upper case'], (actual) { + if (actual != actual.toUpperCase()) { + return Rejection(which: ['was not upper case']); + } + return null; + }); + } + + /// Expects that the string starts with an uppercase letter. + /// + /// {@example /example/core/string/is_capitalized.dart} + void isCapitalized() { + context.expect(() => ['is capitalized'], (actual) { + if (actual.isEmpty) { + return Rejection(which: ['is empty']); + } + final firstChar = actual.substring(0, 1); + if (firstChar != firstChar.toUpperCase()) { + return Rejection(which: ['first character was not upper case']); + } + return null; + }); + } + + /// Expects that the string contains only digits. + /// + /// {@example /example/core/string/is_numeric.dart} + void isNumeric() { + context.expect(() => ['is numeric'], (actual) { + if (!RegExp(r'^\d+$').hasMatch(actual)) { + return Rejection(which: ['contains non-numeric characters']); + } + return null; + }); + } + + /// Expects that the string contains only letters. + /// + /// {@example /example/core/string/is_alpha.dart} + void isAlpha() { + context.expect(() => ['is alpha'], (actual) { + if (!RegExp(r'^[a-zA-Z]+$').hasMatch(actual)) { + return Rejection(which: ['contains non-alphabetic characters']); + } + return null; + }); + } + + /// Expects that the string contains only letters and digits. + /// + /// {@example /example/core/string/is_alphanumeric.dart} + void isAlphanumeric() { + context.expect(() => ['is alphanumeric'], (actual) { + if (!RegExp(r'^[a-zA-Z0-9]+$').hasMatch(actual)) { + return Rejection( + which: ['contains characters that are not letters or digits'], + ); + } + return null; + }); + } + + /// Expects that the string contains [expected] ignoring case. + /// + /// {@example /example/core/string/contains_ignoring_case.dart} + void containsIgnoringCase(String expected) { + context.expect( + () => prefixFirst('contains ignoring case ', literal(expected)), + (actual) { + if (!actual.toLowerCase().contains(expected.toLowerCase())) { + return Rejection( + which: [ + ...prefixFirst( + 'does not contain ', + postfixLast(' (case-insensitive)', literal(expected)), + ), + ], + ); + } + return null; + }, + ); + } +} diff --git a/checks_ext/lib/src/core/stringbuffer.dart b/checks_ext/lib/src/core/stringbuffer.dart new file mode 100644 index 00000000..76157f31 --- /dev/null +++ b/checks_ext/lib/src/core/stringbuffer.dart @@ -0,0 +1,37 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension StringBufferChecksExt on Subject { + /// The string content built by this string buffer. + /// + /// {@example /example/core/stringbuffer/as_string.dart} + Subject get asString => has((b) => b.toString(), 'asString'); + + /// The length of the string content. + Subject get length => has((b) => b.length, 'length'); + + static const _empty = (positive: 'is empty', negative: 'is not empty'); + + /// Expects that the string buffer is empty. + /// + /// {@example /example/core/stringbuffer/is_empty.dart} + void isEmpty() => expectTrue(_empty, (b) => b.isEmpty); + + /// Expects that the string buffer is not empty. + /// + /// {@example /example/core/stringbuffer/is_empty.dart} + void isNotEmpty() => expectFalse(_empty, (b) => b.isEmpty); +} diff --git a/checks_ext/lib/src/core/uri.dart b/checks_ext/lib/src/core/uri.dart new file mode 100644 index 00000000..12bfb4e0 --- /dev/null +++ b/checks_ext/lib/src/core/uri.dart @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension UriChecksExt on Subject { + /// The scheme component of the URI. + Subject get scheme => has((u) => u.scheme, 'scheme'); + + /// The authority component of the URI. + Subject get authority => has((u) => u.authority, 'authority'); + + /// The host component of the URI. + /// + /// {@example /example/core/uri/host.dart} + Subject get host => has((u) => u.host, 'host'); + + /// The port component of the URI. + Subject get port => has((u) => u.port, 'port'); + + /// The path component of the URI. + Subject get path => has((u) => u.path, 'path'); + + /// The query component of the URI. + Subject get query => has((u) => u.query, 'query'); + + /// The fragment component of the URI. + Subject get fragment => has((u) => u.fragment, 'fragment'); + + /// The path segments of the URI. + Subject> get pathSegments => + has((u) => u.pathSegments, 'pathSegments'); + + /// The query parameters of the URI. + Subject> get queryParameters => + has((u) => u.queryParameters, 'queryParameters'); + + /// The query parameters of the URI, with all values for each key. + Subject>> get queryParametersAll => + has((u) => u.queryParametersAll, 'queryParametersAll'); + + /// The data URI content if this is a data URI. + Subject get data => has((u) => u.data, 'data'); + + static const _absolute = ( + positive: 'is absolute', + negative: 'is not absolute', + ); + + /// Expects that the URI is absolute. + void isAbsolute() => expectTrue(_absolute, (u) => u.isAbsolute); + + /// Expects that the URI is not absolute. + void isNotAbsolute() => expectFalse(_absolute, (u) => u.isAbsolute); + + /// Expects that the URI has the given [scheme]. + /// + /// {@example /example/core/uri/has_scheme.dart} + void hasScheme(String scheme) { + context.expect(() => prefixFirst('has scheme ', literal(scheme)), (actual) { + if (actual.scheme == scheme) return null; + return Rejection( + which: [...prefixFirst('has scheme ', literal(actual.scheme))], + ); + }); + } +} diff --git a/checks_ext/lib/src/core/uridata.dart b/checks_ext/lib/src/core/uridata.dart new file mode 100644 index 00000000..80184058 --- /dev/null +++ b/checks_ext/lib/src/core/uridata.dart @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../util.dart'; + +extension UriDataChecksExt on Subject { + /// The MIME type of the data URI. + Subject get mimeType => has((d) => d.mimeType, 'mimeType'); + + /// The charset of the data URI. + Subject get charset => has((d) => d.charset, 'charset'); + + /// The raw text content of the data URI. + Subject get contentText => has((d) => d.contentText, 'contentText'); + + /// Extracts the content of the data URI as a string. + Subject get contentAsString => + has((d) => d.contentAsString(), 'contentAsString'); + + /// Extracts the content of the data URI as bytes. + Subject> get contentAsBytes => + has((d) => d.contentAsBytes(), 'contentAsBytes'); + + /// Expects that the data URI is base64 encoded. + void isBase64() { + context.expect(() => ['is base64'], (actual) { + if (actual.isBase64) return null; + return Rejection(which: ['is not base64']); + }); + } + + /// Expects that the data URI is not base64 encoded. + void isNotBase64() { + context.expect(() => ['is not base64'], (actual) { + if (!actual.isBase64) return null; + return Rejection(which: ['is base64']); + }); + } +} diff --git a/checks_ext/lib/src/math/point.dart b/checks_ext/lib/src/math/point.dart new file mode 100644 index 00000000..b2e39acc --- /dev/null +++ b/checks_ext/lib/src/math/point.dart @@ -0,0 +1,33 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import '../util.dart'; + +extension PointChecksExt on Subject> { + /// The x coordinate of the point. + /// + /// {@example /example/math/point/x.dart} + Subject get x => has((p) => p.x, 'x'); + + /// The y coordinate of the point. + /// + /// {@example /example/math/point/y.dart} + Subject get y => has((p) => p.y, 'y'); + + /// The magnitude of the point (distance from origin). + /// + /// {@example /example/math/point/magnitude.dart} + Subject get magnitude => has((p) => p.magnitude, 'magnitude'); +} diff --git a/checks_ext/lib/src/math/rectangle.dart b/checks_ext/lib/src/math/rectangle.dart new file mode 100644 index 00000000..2c7fe0c7 --- /dev/null +++ b/checks_ext/lib/src/math/rectangle.dart @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import '../util.dart'; + +extension RectangleChecksExt on Subject> { + /// The left coordinate of the rectangle. + /// + /// {@example /example/math/rectangle/left.dart} + Subject get left => has((r) => r.left, 'left'); + + /// The top coordinate of the rectangle. + /// + /// {@example /example/math/rectangle/top.dart} + Subject get top => has((r) => r.top, 'top'); + + /// The width of the rectangle. + /// + /// {@example /example/math/rectangle/width.dart} + Subject get width => has((r) => r.width, 'width'); + + /// The height of the rectangle. + /// + /// {@example /example/math/rectangle/height.dart} + Subject get height => has((r) => r.height, 'height'); + + /// The right coordinate of the rectangle. + /// + /// {@example /example/math/rectangle/right.dart} + Subject get right => has((r) => r.right, 'right'); + + /// The bottom coordinate of the rectangle. + /// + /// {@example /example/math/rectangle/bottom.dart} + Subject get bottom => has((r) => r.bottom, 'bottom'); + + /// Expects that the rectangle contains the given [point]. + /// + /// {@example /example/math/rectangle/contains_point.dart} + void containsPoint(Point point) { + context.expect(() => prefixFirst('contains point ', literal(point)), ( + actual, + ) { + if (actual.containsPoint(point)) return null; + return Rejection( + which: [...prefixFirst('does not contain point ', literal(point))], + ); + }); + } + + /// Expects that the rectangle contains the given [other] rectangle. + /// + /// {@example /example/math/rectangle/contains_rectangle.dart} + void containsRectangle(Rectangle other) { + context.expect(() => prefixFirst('contains rectangle ', literal(other)), ( + actual, + ) { + if (actual.containsRectangle(other)) return null; + return Rejection( + which: [...prefixFirst('does not contain rectangle ', literal(other))], + ); + }); + } +} diff --git a/checks_ext/lib/src/typed_data/bytebuffer.dart b/checks_ext/lib/src/typed_data/bytebuffer.dart new file mode 100644 index 00000000..5629cc03 --- /dev/null +++ b/checks_ext/lib/src/typed_data/bytebuffer.dart @@ -0,0 +1,96 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import '../util.dart'; + +extension ByteBufferChecksExt on Subject { + /// The length of this byte buffer in bytes. + /// + /// {@example /example/typed_data/bytebuffer/length_in_bytes.dart} + Subject get lengthInBytes => + has((b) => b.lengthInBytes, 'lengthInBytes'); + + /// Creates a [Uint8List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_uint8_list.dart} + Subject asUint8List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Uint8List'], + (b) => Extracted.value(b.asUint8List(byteOffset, length)), + ); + + /// Creates a [Int8List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_int8_list.dart} + Subject asInt8List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Int8List'], + (b) => Extracted.value(b.asInt8List(byteOffset, length)), + ); + + /// Creates a [Uint16List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_uint16_list.dart} + Subject asUint16List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Uint16List'], + (b) => Extracted.value(b.asUint16List(byteOffset, length)), + ); + + /// Creates a [Int16List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_int16_list.dart} + Subject asInt16List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Int16List'], + (b) => Extracted.value(b.asInt16List(byteOffset, length)), + ); + + /// Creates a [Uint32List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_uint32_list.dart} + Subject asUint32List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Uint32List'], + (b) => Extracted.value(b.asUint32List(byteOffset, length)), + ); + + /// Creates a [Int32List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_int32_list.dart} + Subject asInt32List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Int32List'], + (b) => Extracted.value(b.asInt32List(byteOffset, length)), + ); + + /// Creates a [Float32List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_float32_list.dart} + Subject asFloat32List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Float32List'], + (b) => Extracted.value(b.asFloat32List(byteOffset, length)), + ); + + /// Creates a [Float64List] view of a region of this byte buffer. + /// + /// {@example /example/typed_data/bytebuffer/as_float64_list.dart} + Subject asFloat64List([int byteOffset = 0, int? length]) => + context.nest( + () => ['as Float64List'], + (b) => Extracted.value(b.asFloat64List(byteOffset, length)), + ); +} diff --git a/checks_ext/lib/src/typed_data/bytedata.dart b/checks_ext/lib/src/typed_data/bytedata.dart new file mode 100644 index 00000000..30a33e65 --- /dev/null +++ b/checks_ext/lib/src/typed_data/bytedata.dart @@ -0,0 +1,94 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import '../util.dart'; + +extension ByteDataChecksExt on Subject { + /// The length of this view in bytes. + /// + /// {@example /example/typed_data/bytedata/length_in_bytes.dart} + Subject get lengthInBytes => + has((b) => b.lengthInBytes, 'lengthInBytes'); + + /// Extracts a Float32 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/float32_at.dart} + Subject float32At(int byteOffset, [Endian endian = Endian.little]) => + context.nest( + () => ['has float32 at $byteOffset'], + (d) => Extracted.value(d.getFloat32(byteOffset, endian)), + ); + + /// Extracts a Float64 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/float64_at.dart} + Subject float64At(int byteOffset, [Endian endian = Endian.little]) => + context.nest( + () => ['has float64 at $byteOffset'], + (d) => Extracted.value(d.getFloat64(byteOffset, endian)), + ); + + /// Extracts a Uint32 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/uint32_at.dart} + Subject uint32At(int byteOffset, [Endian endian = Endian.little]) => + context.nest( + () => ['has uint32 at $byteOffset'], + (d) => Extracted.value(d.getUint32(byteOffset, endian)), + ); + + /// Extracts an Int32 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/int32_at.dart} + Subject int32At(int byteOffset, [Endian endian = Endian.little]) => + context.nest( + () => ['has int32 at $byteOffset'], + (d) => Extracted.value(d.getInt32(byteOffset, endian)), + ); + + /// Extracts a Uint16 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/uint16_at.dart} + Subject uint16At(int byteOffset, [Endian endian = Endian.little]) => + context.nest( + () => ['has uint16 at $byteOffset'], + (d) => Extracted.value(d.getUint16(byteOffset, endian)), + ); + + /// Extracts an Int16 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/int16_at.dart} + Subject int16At(int byteOffset, [Endian endian = Endian.little]) => + context.nest( + () => ['has int16 at $byteOffset'], + (d) => Extracted.value(d.getInt16(byteOffset, endian)), + ); + + /// Extracts a Uint8 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/uint8_at.dart} + Subject uint8At(int byteOffset) => context.nest( + () => ['has uint8 at $byteOffset'], + (d) => Extracted.value(d.getUint8(byteOffset)), + ); + + /// Extracts an Int8 at the given [byteOffset]. + /// + /// {@example /example/typed_data/bytedata/int8_at.dart} + Subject int8At(int byteOffset) => context.nest( + () => ['has int8 at $byteOffset'], + (d) => Extracted.value(d.getInt8(byteOffset)), + ); +} diff --git a/checks_ext/lib/src/typed_data/bytesbuilder.dart b/checks_ext/lib/src/typed_data/bytesbuilder.dart new file mode 100644 index 00000000..d3c4276f --- /dev/null +++ b/checks_ext/lib/src/typed_data/bytesbuilder.dart @@ -0,0 +1,39 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import '../util.dart'; + +extension BytesBuilderChecksExt on Subject { + /// The bytes accumulated by this builder (does not clear). + /// + /// {@example /example/typed_data/bytesbuilder/bytes.dart} + Subject get bytes => + context.nest(() => ['has bytes'], (b) => Extracted.value(b.toBytes())); + + /// The length of the accumulated bytes. + /// + /// {@example /example/typed_data/bytesbuilder/length.dart} + Subject get length => has((b) => b.length, 'length'); + + static const _empty = (positive: 'is empty', negative: 'is not empty'); + + /// Expects that the bytes builder is empty. + /// + /// {@example /example/typed_data/bytesbuilder/is_empty.dart} + void isEmpty() => expectTrue(_empty, (b) => b.isEmpty); + + /// Expects that the bytes builder is not empty. + void isNotEmpty() => expectFalse(_empty, (b) => b.isEmpty); +} diff --git a/checks_ext/lib/src/typed_data/endian.dart b/checks_ext/lib/src/typed_data/endian.dart new file mode 100644 index 00000000..d0d716d6 --- /dev/null +++ b/checks_ext/lib/src/typed_data/endian.dart @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import '../util.dart'; + +extension EndianChecksExt on Subject { + /// Expects that the endianness is the host endianness. + /// + /// {@example /example/typed_data/endian/is_host.dart} + void isHost() { + context.expect(() => ['is Endian.host'], (actual) { + if (actual == Endian.host) return null; + return Rejection( + which: ['is ${actual == Endian.big ? 'Endian.big' : 'Endian.little'}'], + ); + }); + } + + /// Expects that the endianness is big-endian. + /// + /// {@example /example/typed_data/endian/is_big_endian.dart} + void isBigEndian() { + context.expect(() => ['is Endian.big'], (actual) { + if (actual == Endian.big) return null; + return Rejection(which: ['is Endian.little']); + }); + } + + /// Expects that the endianness is little-endian. + /// + /// {@example /example/typed_data/endian/is_little_endian.dart} + void isLittleEndian() { + context.expect(() => ['is Endian.little'], (actual) { + if (actual == Endian.little) return null; + return Rejection(which: ['is Endian.big']); + }); + } +} diff --git a/checks_ext/lib/src/typed_data/simd.dart b/checks_ext/lib/src/typed_data/simd.dart new file mode 100644 index 00000000..ec363518 --- /dev/null +++ b/checks_ext/lib/src/typed_data/simd.dart @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import '../util.dart'; + +extension Float32x4ChecksExt on Subject { + /// The x lane of this vector. + /// + /// {@example /example/typed_data/simd/float32x4.dart} + Subject get x => has((v) => v.x, 'x'); + + /// The y lane of this vector. + /// + /// {@example /example/typed_data/simd/float32x4.dart} + Subject get y => has((v) => v.y, 'y'); + + /// The z lane of this vector. + /// + /// {@example /example/typed_data/simd/float32x4.dart} + Subject get z => has((v) => v.z, 'z'); + + /// The w lane of this vector. + /// + /// {@example /example/typed_data/simd/float32x4.dart} + Subject get w => has((v) => v.w, 'w'); +} + +extension Float64x2ChecksExt on Subject { + /// The x lane of this vector. + /// + /// {@example /example/typed_data/simd/float64x2.dart} + Subject get x => has((v) => v.x, 'x'); + + /// The y lane of this vector. + /// + /// {@example /example/typed_data/simd/float64x2.dart} + Subject get y => has((v) => v.y, 'y'); +} + +extension Int32x4ChecksExt on Subject { + /// The x lane of this vector. + /// + /// {@example /example/typed_data/simd/int32x4.dart} + Subject get x => has((v) => v.x, 'x'); + + /// The y lane of this vector. + /// + /// {@example /example/typed_data/simd/int32x4.dart} + Subject get y => has((v) => v.y, 'y'); + + /// The z lane of this vector. + /// + /// {@example /example/typed_data/simd/int32x4.dart} + Subject get z => has((v) => v.z, 'z'); + + /// The w lane of this vector. + /// + /// {@example /example/typed_data/simd/int32x4.dart} + Subject get w => has((v) => v.w, 'w'); +} diff --git a/checks_ext/lib/src/typed_data/typeddata.dart b/checks_ext/lib/src/typed_data/typeddata.dart new file mode 100644 index 00000000..55873d25 --- /dev/null +++ b/checks_ext/lib/src/typed_data/typeddata.dart @@ -0,0 +1,38 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import '../util.dart'; + +extension TypedDataChecksExt on Subject { + /// The length of this view in bytes. + /// + /// {@example /example/typed_data/typeddata/length_in_bytes.dart} + Subject get lengthInBytes => + has((d) => d.lengthInBytes, 'lengthInBytes'); + + /// The offset of this view from the start of its buffer, in bytes. + /// + /// {@example /example/typed_data/typeddata/offset_in_bytes.dart} + Subject get offsetInBytes => + has((d) => d.offsetInBytes, 'offsetInBytes'); + + /// Creates a [ByteData] view of the region represented by this [TypedData]. + Subject get asByteData => context.nest( + () => ['as ByteData'], + (d) => Extracted.value( + d.buffer.asByteData(d.offsetInBytes, d.lengthInBytes), + ), + ); +} diff --git a/checks_ext/lib/src/util.dart b/checks_ext/lib/src/util.dart new file mode 100644 index 00000000..99899544 --- /dev/null +++ b/checks_ext/lib/src/util.dart @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks/context.dart'; + +export 'package:checks/checks.dart'; +export 'package:checks/context.dart'; + +extension Expect on Subject { + void expectTrue(Description description, bool Function(T) predicate) => + context.expect(() => [description.positive], (s) { + if (predicate(s)) return null; + return Rejection(which: [description.negative]); + }); + + void expectFalse(Description description, bool Function(T) predicate) => + context.expect(() => [description.negative], (s) { + if (!predicate(s)) return null; + return Rejection(which: [description.positive]); + }); +} + +typedef Description = ({String positive, String negative}); + +String prettyPrintDuration(Duration duration) { + if (duration == Duration.zero) return '0s'; + final isNegative = duration.isNegative; + final absDuration = duration.abs(); + + final parts = []; + + final days = absDuration.inDays; + if (days > 0) parts.add('$days day${days > 1 ? 's' : ''}'); + + final hours = absDuration.inHours % 24; + if (hours > 0) parts.add('$hours hour${hours > 1 ? 's' : ''}'); + + final minutes = absDuration.inMinutes % 60; + if (minutes > 0) parts.add('${minutes}m'); + + final seconds = absDuration.inSeconds % 60; + if (seconds > 0) parts.add('${seconds}s'); + + final ms = absDuration.inMilliseconds % 1000; + if (ms > 0) parts.add('${ms}ms'); + + final us = absDuration.inMicroseconds % 1000; + if (us > 0) parts.add('${us}us'); + + final result = parts.join(' '); + return isNegative ? '-$result' : result; +} diff --git a/checks_ext/pubspec.yaml b/checks_ext/pubspec.yaml new file mode 100644 index 00000000..690ae0d6 --- /dev/null +++ b/checks_ext/pubspec.yaml @@ -0,0 +1,12 @@ +name: checks_ext +description: Extensions for package:checks to make testing standard library types easier and more expressive. +version: 0.0.0 +publish_to: none +environment: + sdk: ^3.12.0 +dependencies: + checks: ^0.3.1 + collection: ^1.19.1 +dev_dependencies: + test: ^1.31.1 + stack_trace: ^1.12.1 diff --git a/checks_ext/test/check_examples.dart b/checks_ext/test/check_examples.dart new file mode 100644 index 00000000..b14552df --- /dev/null +++ b/checks_ext/test/check_examples.dart @@ -0,0 +1,100 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A test helper to check that your examples files print output matching their `//`-comments. +/// This ensures your documentation stays up to date! +/// +/// To use it, create a file `test/examples_test.dart`: +/// +/// ```dart +/// import 'check_examples.dart'; +/// +/// void main() => checkExamples(); +/// ``` +/// +/// Then write your example files in the `example/` directory like this: +/// +/// ```dart +/// void main() { +/// /// Prints hello world (this line is not part of expected output) +/// print('hello world'); +/// // hello world +/// } +/// ``` +/// +/// The test runner will run the example and verify that the combined stdout/stderr +/// matches lines starting with `//` (ignoring `///`). Empty lines are ignored. +/// +/// ### Dartdoc `@example` Directive +/// +/// To include these examples in your dartdoc generated documentation, +/// use the `{@example}` directive: +/// +/// ```dart +/// /// My function that does stuff... +/// /// +/// /// {@example /example/my_function_example.dart} +/// void myFunction() { ... } +/// ``` +/// +/// This will pull the code from the example file into the generated documentation! +library; + +import 'dart:convert'; +import 'dart:io'; +import 'package:checks/checks.dart'; +import 'package:test/test.dart' show group, test; + +void checkExamples({String directory = 'example'}) { + group('Examples in "$directory"', () { + final exampleDir = Directory(directory); + if (!exampleDir.existsSync()) return; + + final exampleFiles = exampleDir + .listSync(recursive: true) + .whereType() + .where((f) => f.path.endsWith('.dart')); + + for (final file in exampleFiles) { + final filename = file.uri.pathSegments.last; + + test('"$filename" output matches comments', () async { + final expected = file + .readAsLinesSync() + .map((l) => l.trimLeft()) + .where((l) => l.startsWith('//') && !l.startsWith('///')) + .map((l) => l.startsWith('// ') ? l.substring(3) : l.substring(2)) + .where((l) => l.trim().isNotEmpty) + .join('\n'); + + // Run the example as a process + final p = await Process.start(Platform.resolvedExecutable, [file.path]); + + final outputBuffer = StringBuffer(); + await Future.wait([ + p.stdout.transform(utf8.decoder).forEach(outputBuffer.write), + p.stderr.transform(utf8.decoder).forEach(outputBuffer.write), + p.exitCode, + ]); + + final output = const LineSplitter() + .convert(outputBuffer.toString()) + .where((l) => l.trim().isNotEmpty) + .join('\n'); + + check(output).equals(expected); + }); + } + }); +} diff --git a/checks_ext/test/core/bigint_test.dart b/checks_ext/test/core/bigint_test.dart new file mode 100644 index 00000000..662bd6c0 --- /dev/null +++ b/checks_ext/test/core/bigint_test.dart @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/bigint.dart'; +import '../util.dart'; + +void main() { + group('BigIntChecks', () { + test('bitLength extracts bitLength', () { + check(BigInt.from(5)).bitLength.equals(3); // 5 is 101 in binary + }); + + test('sign extracts sign', () { + check(BigInt.from(5)).sign.equals(1); + check(BigInt.from(-5)).sign.equals(-1); + check(BigInt.zero).sign.equals(0); + }); + + test('isEven succeeds', () { + check(BigInt.from(2)).isEven(); + }); + + test('isEven fails', () { + check( + BigInt.from(1), + ).isRejectedBy((it) => it.isEven(), which: ['is not even']); + }); + + test('isNotEven succeeds', () { + check(BigInt.from(1)).isNotEven(); + }); + + test('isNotEven fails', () { + check( + BigInt.from(2), + ).isRejectedBy((it) => it.isNotEven(), which: ['is even']); + }); + + test('isOdd succeeds', () { + check(BigInt.from(1)).isOdd(); + }); + + test('isOdd fails', () { + check( + BigInt.from(2), + ).isRejectedBy((it) => it.isOdd(), which: ['is not odd']); + }); + + test('isNotOdd succeeds', () { + check(BigInt.from(2)).isNotOdd(); + }); + + test('isNotOdd fails', () { + check( + BigInt.from(1), + ).isRejectedBy((it) => it.isNotOdd(), which: ['is odd']); + }); + + test('isNegative succeeds', () { + check(BigInt.from(-1)).isNegative(); + }); + + test('isNegative fails', () { + check( + BigInt.from(1), + ).isRejectedBy((it) => it.isNegative(), which: ['is not negative']); + }); + + test('isValidInt succeeds', () { + check(BigInt.from(42)).isValidInt(); + }); + + test('isValidInt fails', () { + final huge = BigInt.parse('10000000000000000000000000000000000000000'); + check( + huge, + ).isRejectedBy((it) => it.isValidInt(), which: ['is not valid int']); + }); + + testCheckGolden( + () { + check(BigInt.from(1)).isEven(); + }, + ''' +# isEven failure message golden +Expected: a BigInt that: + is even +Actual: <1> +Which: is not even''', + ); + }); +} diff --git a/checks_ext/test/core/comparable_test.dart b/checks_ext/test/core/comparable_test.dart new file mode 100644 index 00000000..0f131920 --- /dev/null +++ b/checks_ext/test/core/comparable_test.dart @@ -0,0 +1,265 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/comparable.dart'; + +import '../util.dart'; + +void main() { + group('isSorted', () { + test('succeeds for sorted list', () { + check([1, 2, 3]).isSorted(); + }); + + test('succeeds for empty list', () { + check([]).isSorted(); + }); + + test('succeeds for one element', () { + check([42]).isSorted(); + }); + + test('fails for unsorted list', () { + check([2, 1]).isRejectedBy( + (it) => it.isSorted(), + which: [ + 'is not sorted, because element at index 0 <2>', + 'compares after element at index 1 <1>', + ], + ); + }); + + test('succeeds for sorted letters', () { + check(['a', 'b', 'c']).isSorted(); + }); + + test('fails for unsorted letters', () { + check(['b', 'a']).isRejectedBy( + (it) => it.isSorted(), + which: [ + "is not sorted, because element at index 0 'b'", + "compares after element at index 1 'a'", + ], + ); + }); + + testCheckGolden( + () { + check([2, 1]).isSorted(); + }, + ''' +# isSorted failure message golden +Expected: a List that: + is sorted +Actual: [2, 1] +Which: is not sorted, because element at index 0 <2> +compares after element at index 1 <1>''', + ); + }); + + group('IterableComparableChecks', () { + test('max succeeds', () { + check([1, 3, 2]).max.equals(3); + }); + test('max rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.max.equals(0), which: ['is empty']); + }); + test('max fails for wrong expectation', () { + check([1, 3, 2]).isRejectedBy( + (it) => it.max.equals(4), + actual: ['<3>'], + which: ['are not equal'], + ); + }); + test('min succeeds', () { + check([2, 1, 3]).min.equals(1); + }); + test('min rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.min.equals(0), which: ['is empty']); + }); + test('min fails for wrong expectation', () { + check([2, 1, 3]).isRejectedBy( + (it) => it.min.equals(2), + actual: ['<1>'], + which: ['are not equal'], + ); + }); + }); + + group('IterableDoubleChecks', () { + test('max succeeds', () { + check([1.0, 3.0, 2.0]).max.equals(3.0); + }); + test('max rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.max.equals(0.0), which: ['is empty']); + }); + test('max fails for wrong expectation', () { + check([1.0, 3.0, 2.0]).isRejectedBy( + (it) => it.max.equals(4.0), + actual: ['<3.0>'], + which: ['are not equal'], + ); + }); + test('min succeeds', () { + check([2.0, 1.0, 3.0]).min.equals(1.0); + }); + test('min rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.min.equals(0.0), which: ['is empty']); + }); + test('min fails for wrong expectation', () { + check([2.0, 1.0, 3.0]).isRejectedBy( + (it) => it.min.equals(2.0), + actual: ['<1.0>'], + which: ['are not equal'], + ); + }); + test('sum succeeds', () { + check([1.0, 2.0]).sum.equals(3.0); + }); + test('sum succeeds for empty', () { + check([]).sum.equals(0.0); + }); + test('sum fails for wrong expectation', () { + check([1.0, 2.0]).isRejectedBy( + (it) => it.sum.equals(4.0), + actual: ['<3.0>'], + which: ['are not equal'], + ); + }); + }); + + group('IterableIntegerChecks', () { + test('max succeeds', () { + check([1, 3, 2]).max.equals(3); + }); + test('max rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.max.equals(0), which: ['is empty']); + }); + test('max fails for wrong expectation', () { + check([1, 3, 2]).isRejectedBy( + (it) => it.max.equals(4), + actual: ['<3>'], + which: ['are not equal'], + ); + }); + test('min succeeds', () { + check([2, 1, 3]).min.equals(1); + }); + test('min rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.min.equals(0), which: ['is empty']); + }); + test('min fails for wrong expectation', () { + check([2, 1, 3]).isRejectedBy( + (it) => it.min.equals(2), + actual: ['<1>'], + which: ['are not equal'], + ); + }); + test('sum succeeds', () { + check([1, 2]).sum.equals(3); + }); + test('sum fails for wrong expectation', () { + check([1, 2]).isRejectedBy( + (it) => it.sum.equals(4), + actual: ['<3>'], + which: ['are not equal'], + ); + }); + test('average succeeds', () { + check([1, 3]).average.equals(2.0); + }); + test('average rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.average.equals(0.0), which: ['is empty']); + }); + test('average fails for wrong expectation', () { + check([1, 3]).isRejectedBy( + (it) => it.average.equals(3.0), + actual: ['<2.0>'], + which: ['are not equal'], + ); + }); + }); + + group('IterableNumberChecks', () { + test('max succeeds', () { + check([1, 3.0, 2]).max.equals(3.0); + }); + test('max rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.max.equals(0), which: ['is empty']); + }); + test('max fails for wrong expectation', () { + check([1, 3.0, 2]).isRejectedBy( + (it) => it.max.equals(4.0), + actual: ['<3.0>'], + which: ['are not equal'], + ); + }); + test('min succeeds', () { + check([2, 1.0, 3]).min.equals(1.0); + }); + test('min rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.min.equals(0), which: ['is empty']); + }); + test('min fails for wrong expectation', () { + check([2, 1.0, 3]).isRejectedBy( + (it) => it.min.equals(2.0), + actual: ['<1.0>'], + which: ['are not equal'], + ); + }); + test('sum succeeds', () { + check([1, 2.0]).sum.equals(3.0); + }); + test('sum fails for wrong expectation', () { + check([1, 2.0]).isRejectedBy( + (it) => it.sum.equals(4.0), + actual: ['<3.0>'], + which: ['are not equal'], + ); + }); + test('average succeeds', () { + check([1, 3.0]).average.equals(2.0); + }); + test('average rejects empty', () { + check( + [], + ).isRejectedBy((it) => it.average.equals(0.0), which: ['is empty']); + }); + test('average fails for wrong expectation', () { + check([1, 3.0]).isRejectedBy( + (it) => it.average.equals(3.0), + actual: ['<2.0>'], + which: ['are not equal'], + ); + }); + }); +} diff --git a/checks_ext/test/core/datetime_test.dart b/checks_ext/test/core/datetime_test.dart new file mode 100644 index 00000000..03041ab5 --- /dev/null +++ b/checks_ext/test/core/datetime_test.dart @@ -0,0 +1,155 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/datetime.dart'; +import '../util.dart'; + +void main() { + group('DateTimeChecks', () { + final now = DateTime.utc(2020, 1, 2); + final past = DateTime.utc(2020, 1, 1); + final future = DateTime.utc(2020, 1, 3); + + test('isAfter succeeds', () { + check(future).isAfter(now); + }); + + test('isAfter fails', () { + check(past).isRejectedBy( + (it) => it.isAfter(now), + which: ['was not after <2020-01-02 00:00:00.000Z>'], + ); + }); + + test('isBefore succeeds', () { + check(past).isBefore(now); + }); + + test('isBefore fails', () { + check(future).isRejectedBy( + (it) => it.isBefore(now), + which: ['was not before <2020-01-02 00:00:00.000Z>'], + ); + }); + + test('isAtSameMomentAs succeeds', () { + check(now).isAtSameMomentAs(now); + }); + + test('isAtSameMomentAs fails', () { + check(past).isRejectedBy( + (it) => it.isAtSameMomentAs(now), + which: ['was not at same moment as <2020-01-02 00:00:00.000Z>'], + ); + }); + + test('isUtc succeeds', () { + check(DateTime.utc(2020)).isUtc(); + }); + + test('isUtc fails', () { + check( + DateTime(2020), + ).isRejectedBy((it) => it.isUtc(), which: ['is not in UTC']); + }); + + test('isNotUtc succeeds', () { + check(DateTime(2020)).isNotUtc(); + }); + + test('isNotUtc fails', () { + check( + DateTime.utc(2020), + ).isRejectedBy((it) => it.isNotUtc(), which: ['is UTC']); + }); + + test('year extracts year', () { + check(DateTime(2020)).year.equals(2020); + }); + + test('weekday extracts weekday', () { + // 2020-01-01 was Wednesday (3) + check(DateTime.utc(2020, 1, 1)).weekday.equals(3); + }); + + test('difference calculates duration', () { + check(future).difference(now).equals(Duration(days: 1)); + }); + + test('isCloseTo succeeds', () { + final d1 = DateTime.utc(2020, 1, 1, 10, 0, 0); + final d2 = DateTime.utc(2020, 1, 1, 10, 0, 5); + check(d2).isCloseTo(d1, within: Duration(seconds: 10)); + }); + + testCheckGolden( + () { + final d1 = DateTime.utc(2020, 1, 1, 10, 0, 0); + final d2 = DateTime.utc(2020, 1, 1, 10, 0, 15); + check(d2).isCloseTo(d1, within: Duration(seconds: 10)); + }, + ''' +# isCloseTo failure message golden +Expected: a DateTime that: + is close to <2020-01-01 10:00:00.000Z> within 10s +Actual: <2020-01-01 10:00:15.000Z> +Which: differs by 15s +which is more than 10s''', + ); + + test('isBetween succeeds (inclusive)', () { + final start = DateTime.utc(2020, 1, 1); + final end = DateTime.utc(2020, 1, 3); + check(DateTime.utc(2020, 1, 2)).isBetween(start, end); + check(start).isBetween(start, end); + check(end).isBetween(start, end); + }); + + test('isBetween succeeds (exclusive)', () { + final start = DateTime.utc(2020, 1, 1); + final end = DateTime.utc(2020, 1, 3); + check(DateTime.utc(2020, 1, 2)).isBetween(start, end, inclusive: false); + }); + + testCheckGolden( + () { + final start = DateTime.utc(2020, 1, 1); + final end = DateTime.utc(2020, 1, 3); + check(start).isBetween(start, end, inclusive: false); + }, + ''' +# isBetween failure message golden +Expected: a DateTime that: + is between <2020-01-01 00:00:00.000Z> + and <2020-01-03 00:00:00.000Z> + (exclusive) +Actual: <2020-01-01 00:00:00.000Z> +Which: was not between <2020-01-01 00:00:00.000Z> +and <2020-01-03 00:00:00.000Z> + (exclusive)''', + ); + + testCheckGolden( + () { + check(past).isAfter(now); + }, + ''' +# isAfter failure message golden +Expected: a DateTime that: + is after <2020-01-02 00:00:00.000Z> +Actual: <2020-01-01 00:00:00.000Z> +Which: was not after <2020-01-02 00:00:00.000Z>''', + ); + }); +} diff --git a/checks_ext/test/core/duration_test.dart b/checks_ext/test/core/duration_test.dart new file mode 100644 index 00000000..09107cca --- /dev/null +++ b/checks_ext/test/core/duration_test.dart @@ -0,0 +1,157 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/duration.dart'; +import '../util.dart'; + +void main() { + group('DurationChecks', () { + test('isGreaterThan succeeds', () { + check(Duration(seconds: 10)).isGreaterThan(Duration(seconds: 5)); + }); + + test('isGreaterThan fails', () { + check(Duration(seconds: 5)).isRejectedBy( + (it) => it.isGreaterThan(Duration(seconds: 10)), + which: ['was not greater than 10s', 'actual was 5s'], + ); + }); + + test('isLessThan succeeds', () { + check(Duration(seconds: 5)).isLessThan(Duration(seconds: 10)); + }); + + test('isLessThan fails', () { + check(Duration(seconds: 10)).isRejectedBy( + (it) => it.isLessThan(Duration(seconds: 5)), + which: ['was not less than 5s', 'actual was 10s'], + ); + }); + + test('isGreaterOrEqual succeeds', () { + check(Duration(seconds: 10)).isGreaterOrEqual(Duration(seconds: 5)); + check(Duration(seconds: 5)).isGreaterOrEqual(Duration(seconds: 5)); + }); + + test('isGreaterOrEqual fails', () { + check(Duration(seconds: 5)).isRejectedBy( + (it) => it.isGreaterOrEqual(Duration(seconds: 10)), + which: ['was not greater than or equal to 10s', 'actual was 5s'], + ); + }); + + test('isLessOrEqual succeeds', () { + check(Duration(seconds: 5)).isLessOrEqual(Duration(seconds: 10)); + check(Duration(seconds: 5)).isLessOrEqual(Duration(seconds: 5)); + }); + + test('isLessOrEqual fails', () { + check(Duration(seconds: 10)).isRejectedBy( + (it) => it.isLessOrEqual(Duration(seconds: 5)), + which: ['was not less than or equal to 5s', 'actual was 10s'], + ); + }); + + test('isCloseTo succeeds', () { + check( + Duration(seconds: 12), + ).isCloseTo(Duration(seconds: 10), within: Duration(seconds: 5)); + }); + + test('isCloseTo fails', () { + check(Duration(seconds: 20)).isRejectedBy( + (it) => + it.isCloseTo(Duration(seconds: 10), within: Duration(seconds: 5)), + which: ['differs by 10s', 'which is more than 5s'], + ); + }); + + test('isZero succeeds', () { + check(Duration.zero).isZero(); + }); + + test('isZero fails', () { + check( + Duration(seconds: 1), + ).isRejectedBy((it) => it.isZero(), which: ['is not zero']); + }); + + testCheckGolden( + () { + check(Duration(seconds: 5)).isGreaterThan(Duration(seconds: 10)); + }, + ''' +# isGreaterThan failure message golden +Expected: a Duration that: + is greater than 10s +Actual: <0:00:05.000000> +Which: was not greater than 10s +actual was 5s''', + ); + + test('inDays extracts total days', () { + check(Duration(hours: 48)).inDays.equals(2); + }); + + test('inHours extracts total hours', () { + check(Duration(days: 2)).inHours.equals(48); + }); + + test('inMinutes extracts total minutes', () { + check(Duration(hours: 2)).inMinutes.equals(120); + }); + + test('inSeconds extracts total seconds', () { + check(Duration(minutes: 2)).inSeconds.equals(120); + }); + + test('inMilliseconds extracts total milliseconds', () { + check(Duration(seconds: 2)).inMilliseconds.equals(2000); + }); + + test('inMicroseconds extracts total microseconds', () { + check(Duration(milliseconds: 2)).inMicroseconds.equals(2000); + }); + + testCheckGolden( + () { + check(Duration(hours: 24)).inDays.equals(2); + }, + ''' +# inDays failure message golden +Expected: a Duration that: + in days that: + equals <2> +Actual: a Duration that: + in days that: + Actual: <1> + Which: are not equal''', + ); + + testCheckGolden( + () { + check( + Duration(seconds: 20), + ).isCloseTo(Duration(seconds: 10), within: Duration(seconds: 5)); + }, + ''' +# isCloseTo failure message golden +Expected: a Duration that: + is close to 10s within 5s +Actual: <0:00:20.000000> +Which: differs by 10s +which is more than 5s''', + ); + }); +} diff --git a/checks_ext/test/core/enum_test.dart b/checks_ext/test/core/enum_test.dart new file mode 100644 index 00000000..33f91308 --- /dev/null +++ b/checks_ext/test/core/enum_test.dart @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/enum.dart'; +import '../util.dart'; + +enum TestEnum { foo, bar } + +void main() { + group('EnumChecks', () { + test('index extracts zero-based position', () { + check(TestEnum.foo).index.equals(0); + check(TestEnum.bar).index.equals(1); + }); + + test('name extracts name of enum constant', () { + check(TestEnum.foo).name.equals('foo'); + check(TestEnum.bar).name.equals('bar'); + }); + + test('index fails for wrong expectation', () { + check(TestEnum.foo).isRejectedBy( + (it) => it.index.equals(1), + actual: ['<0>'], + which: ['are not equal'], + ); + }); + + test('name fails for wrong expectation', () { + check(TestEnum.foo).isRejectedBy( + (it) => it.name.equals('bar'), + actual: ["'foo'"], + which: ['differs at offset 0:', 'bar', 'foo', '^'], + ); + }); + + testCheckGolden( + () { + check(TestEnum.foo).name.equals('bar'); + }, + ''' +# Enum name failure message golden +Expected: a TestEnum that: + has name that: + equals 'bar' +Actual: a TestEnum that: + has name that: + Actual: 'foo' + Which: differs at offset 0: + bar + foo + ^''', + ); + }); +} diff --git a/checks_ext/test/core/iterable_test.dart b/checks_ext/test/core/iterable_test.dart new file mode 100644 index 00000000..c267bce9 --- /dev/null +++ b/checks_ext/test/core/iterable_test.dart @@ -0,0 +1,285 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/iterable.dart'; +import 'package:collection/collection.dart'; + +import '../util.dart'; + +void main() { + group('isSortedBy', () { + test('succeeds for sorted maps by key', () { + check([ + {'age': 20}, + {'age': 30}, + ]).isSortedBy((m) => m['age'] as int); + }); + + test('fails for unsorted maps by key', () { + check([ + {'age': 30}, + {'age': 20}, + ]).isRejectedBy( + (it) => it.isSortedBy((m) => m['age'] as int), + which: [ + "is not sorted, because element at index 0 {'age': 30}", + "with key <30>", + "compares after element at index 1 {'age': 20}", + "with key <20>", + ], + ); + }); + + test('succeeds for empty list', () { + check(>[]).isSortedBy((m) => m['age'] as int); + }); + + test('succeeds for one element', () { + check([ + {'age': 42}, + ]).isSortedBy((m) => m['age'] as int); + }); + + testCheckGolden( + () { + check([ + {'age': 30}, + {'age': 20}, + ]).isSortedBy((m) => m['age'] as int); + }, + ''' +# isSortedBy failure message golden +Expected: a List> that: + is sorted by key +Actual: [{'age': 30}, {'age': 20}] +Which: is not sorted, because element at index 0 {'age': 30} +with key <30> +compares after element at index 1 {'age': 20} +with key <20>''', + ); + + testCheckGolden( + () { + check([ + {'age': 30}, + {'age': 20}, + ]).isSortedBy((m) => m['age'] as int, 'age'); + }, + ''' +# isSortedBy with keyName failure message golden +Expected: a List> that: + is sorted by key "age" +Actual: [{'age': 30}, {'age': 20}] +Which: is not sorted, because element at index 0 {'age': 30} +with key "age" <30> +compares after element at index 1 {'age': 20} +with key "age" <20>''', + ); + + testCheckGolden( + () { + check([ + { + 'name': 'Bob Builder', + 'age': 34, + 'species': 'human', + 'employment': 'construction', + 'location': 'Builderland', + 'hobbies': ['building', 'fixing'], + }, + { + 'name': 'Alice Architect', + 'age': 30, + 'species': 'human', + 'employment': 'design', + 'location': 'Builderland', + 'hobbies': ['drawing', 'planning'], + }, + ]).isSortedBy((m) => m['age'] as int, 'age'); + }, + ''' +# isSortedBy with large entries failure message golden +Expected: a List> that: + is sorted by key "age" +Actual: [{'name': 'Bob Builder', +'age': 34, +'species': 'human', +'employment': 'construction', +'location': 'Builderland', +'hobbies': ['building', 'fixing']}, +{'name': 'Alice Architect', +'age': 30, +'species': 'human', +'employment': 'design', +'location': 'Builderland', +'hobbies': ['drawing', 'planning']}] +Which: is not sorted, because element at index 0 {'name': 'Bob Builder', +'age': 34, +'species': 'human', +'employment': 'construction', +'location': 'Builderland', +'hobbies': ['building', 'fixing']} +with key "age" <34> +compares after element at index 1 {'name': 'Alice Architect', +'age': 30, +'species': 'human', +'employment': 'design', +'location': 'Builderland', +'hobbies': ['drawing', 'planning']} +with key "age" <30>''', + ); + }); + + group('containsAll', () { + test('succeeds when all elements are present', () { + check([1, 2, 3]).containsAll([1, 2]); + check([1, 2, 3]).containsAll([3, 1]); + }); + + test('fails when elements are missing', () { + check([1, 2, 3]).isRejectedBy( + (it) => it.containsAll([1, 4]), + which: ["is missing elements: [4]"], + ); + }); + + testCheckGolden( + () { + check([1, 2, 3]).containsAll([1, 4]); + }, + ''' +# containsAll failure message golden +Expected: a List that: + contains all of [1, 4] +Actual: [1, 2, 3] +Which: is missing elements: [4]''', + ); + testCheckGolden( + () { + final myMap = {'a': 1, 'b': 2}; + check(myMap).has((m) => m.keys, 'keys').containsAll(['a', 'c']); + }, + ''' +# containsAll with map keys failure message golden +Expected: a Map that: + has keys that: + contains all of ['a', 'c'] +Actual: a Map that: + has keys that: + Actual: ('a', 'b') + Which: is missing elements: ['c']''', + ); + }); + + group('NullableIterableChecks', () { + test('nonNulls extracts non-null elements', () { + check([1, null, 2]).nonNulls.deepEquals([1, 2]); + }); + + testCheckGolden( + () { + check([1, null, 2]).nonNulls.deepEquals([1, 3]); + }, + ''' +# nonNulls failure message golden +Expected: a List that: + skipping nulls that: + is deeply equal to [1, 3] +Actual: a List that: + skipping nulls that: + Actual: (1, 2) + Which: at [<1>] is <2> + which does not equal <3>''', + ); + }); + + group('containsSequence', () { + test('succeeds when containing sequence', () { + check([1, 2, 3, 4, 5]).containsSequence([2, 3, 4]); + }); + + test('fails when not containing sequence', () { + check([1, 2, 3, 4, 5]).isRejectedBy( + (it) => it.containsSequence([2, 4]), + which: ['does not contain the sequence'], + ); + }); + + testCheckGolden( + () { + check([1, 2, 3]).containsSequence([2, 4]); + }, + ''' +# containsSequence failure message golden +Expected: a List that: + contains sequence [2, 4] +Actual: [1, 2, 3] +Which: does not contain the sequence''', + ); + }); + + group('hasDuplicates', () { + test('succeeds when having duplicates', () { + check([1, 2, 2, 3]).hasDuplicates(); + }); + + test('fails when having no duplicates', () { + check([ + 1, + 2, + 3, + ]).isRejectedBy((it) => it.hasDuplicates(), which: ['has no duplicates']); + }); + + test('works with custom equality', () { + check([ + 'a', + 'A', + ]).hasDuplicates(equality: EqualityBy((s) => s.toLowerCase())); + }); + }); + + group('hasNoDuplicates', () { + test('succeeds when having no duplicates', () { + check([1, 2, 3]).hasNoDuplicates(); + }); + + test('fails when having duplicates', () { + check([1, 2, 2, 3]).isRejectedBy( + (it) => it.hasNoDuplicates(), + which: ["has duplicates: [2]"], + ); + }); + + test('works with custom equality', () { + check(['a', 'A']).isRejectedBy( + (it) => + it.hasNoDuplicates(equality: EqualityBy((s) => s.toLowerCase())), + which: ["has duplicates: ['A']"], + ); + }); + + testCheckGolden( + () { + check([1, 2, 2, 3, 3]).hasNoDuplicates(); + }, + ''' +# hasNoDuplicates failure message golden +Expected: a List that: + has no duplicates +Actual: [1, 2, 2, 3, 3] +Which: has duplicates: [2, 3]''', + ); + }); +} diff --git a/checks_ext/test/core/list_test.dart b/checks_ext/test/core/list_test.dart new file mode 100644 index 00000000..387be605 --- /dev/null +++ b/checks_ext/test/core/list_test.dart @@ -0,0 +1,135 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:checks_ext/src/core/list.dart'; +import '../util.dart'; + +void main() { + group('Uint8ListChecks', () { + test('asUtf8 decodes string', () { + final bytes = Uint8List.fromList(utf8.encode('hello')); + check(bytes).asUtf8.equals('hello'); + }); + + test('asUtf8 fails for invalid UTF-8', () { + final bytes = Uint8List.fromList([0xFF, 0xFE]); + check(bytes).isRejectedBy( + (it) => it.asUtf8.equals(''), + which: ['is not valid UTF-8'], + ); + }); + + test('isValidUtf8 succeeds for valid UTF-8', () { + final bytes = Uint8List.fromList(utf8.encode('hello')); + check(bytes).isValidUtf8(); + }); + + test('isValidUtf8 fails for invalid UTF-8', () { + final bytes = Uint8List.fromList([0xFF, 0xFE]); + check( + bytes, + ).isRejectedBy((it) => it.isValidUtf8(), which: ['is not valid UTF-8']); + }); + + test('containsBytes succeeds when containing sequence', () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); + check(bytes).containsBytes([2, 3, 4]); + }); + + test('containsBytes fails when not containing sequence', () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); + check(bytes).isRejectedBy( + (it) => it.containsBytes([2, 4]), + which: [ + 'does not contain the sequence: 02 04', + 'Actual bytes: 01 02 03 04 05', + ], + ); + }); + + test('equalsBytes succeeds when equal', () { + final bytes = Uint8List.fromList([1, 2, 3]); + check(bytes).equalsBytes([1, 2, 3]); + }); + + testCheckGolden( + () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); + check(bytes).equalsBytes([1, 2, 99, 4, 5]); + }, + ''' +# equalsBytes failure message golden +Expected: a Uint8List that: + equals [1, 2, 99, 4, 5] +Actual: [1, 2, 3, 4, 5] +Which: differs at offset 2: +Actual: 01 02 [03] 04 05 +Expected: 01 02 [63] 04 05''', + ); + + testCheckGolden( + () { + final bytes = Uint8List.fromList(utf8.encode('hello world')); + check( + bytes, + ).equalsBytes(Uint8List.fromList(utf8.encode('hello World'))); + }, + ''' +# equalsBytes failure message golden with ASCII +Expected: a Uint8List that: + equals [104, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100] +Actual: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] +Which: differs at offset 6: +Actual: ... e l l o [w] o r l d +Expected: ... e l l o [W] o r l d''', + ); + + testCheckGolden( + () { + final bytes = Uint8List.fromList(utf8.encode('hello\nworld')); + check( + bytes, + ).equalsBytes(Uint8List.fromList(utf8.encode('hello\nWorld'))); + }, + ''' +# equalsBytes failure message golden with control character falls back to hex +Expected: a Uint8List that: + equals [104, 101, 108, 108, 111, 10, 87, 111, 114, 108, 100] +Actual: [104, 101, 108, 108, 111, 10, 119, 111, 114, 108, 100] +Which: differs at offset 6: +Actual: ... 65 6c 6c 6f 0a [77] 6f 72 6c 64 +Expected: ... 65 6c 6c 6f 0a [57] 6f 72 6c 64''', + ); + test('works on regular List', () { + final list = [104, 101, 108, 108, 111]; + check(list).asUtf8.equals('hello'); + }); + + testCheckGolden( + () { + final bytes = Uint8List.fromList(utf8.encode('hello world')); + check(bytes).containsBytes(Uint8List.fromList(utf8.encode('word'))); + }, + ''' +# containsBytes failure message golden with ASCII +Expected: a Uint8List that: + contains bytes [119, 111, 114, 100] +Actual: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] +Which: does not contain the sequence: w o r d +Actual bytes: h e l l o w o r l d''', + ); + }); +} diff --git a/checks_ext/test/core/match_test.dart b/checks_ext/test/core/match_test.dart new file mode 100644 index 00000000..1e25a6aa --- /dev/null +++ b/checks_ext/test/core/match_test.dart @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/match.dart'; +import '../util.dart'; + +void main() { + group('MatchChecks', () { + final regExp = RegExp(r'(\w+) (\d+)'); + const input = 'hello 42'; + final match = regExp.firstMatch(input)!; + + test('properties extract correct values', () { + check(match) + ..start.equals(0) + ..end.equals(8) + ..groupCount.equals(2) + ..input.equals(input); + }); + + test('group extracts correct group', () { + check(match) + ..group(0).equals('hello 42') + ..group(1).equals('hello') + ..group(2).equals('42'); + }); + + test('operator [] works like group', () { + check(match) + ..[0].equals('hello 42') + ..[1].equals('hello') + ..[2].equals('42'); + }); + }); +} diff --git a/checks_ext/test/core/num_test.dart b/checks_ext/test/core/num_test.dart new file mode 100644 index 00000000..01ec3d94 --- /dev/null +++ b/checks_ext/test/core/num_test.dart @@ -0,0 +1,110 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/num.dart'; +import '../util.dart'; + +void main() { + group('NumChecks', () { + test('isStrictlyPositive succeeds for positive numbers', () { + check(1).isStrictlyPositive(); + check(0.5).isStrictlyPositive(); + }); + + test('isStrictlyPositive fails for zero and negative numbers', () { + check(0).isRejectedBy( + (it) => it.isStrictlyPositive(), + which: ['is not strictly positive'], + ); + check(-1).isRejectedBy( + (it) => it.isStrictlyPositive(), + which: ['is not strictly positive'], + ); + }); + + test('isStrictlyNegative succeeds for negative numbers', () { + check(-1).isStrictlyNegative(); + check(-0.5).isStrictlyNegative(); + }); + + test('isStrictlyNegative fails for zero and positive numbers', () { + check(0).isRejectedBy( + (it) => it.isStrictlyNegative(), + which: ['is not strictly negative'], + ); + check(1).isRejectedBy( + (it) => it.isStrictlyNegative(), + which: ['is not strictly negative'], + ); + }); + + test('isBetween succeeds within range', () { + check(1).isBetween(0, 2); + check(0).isBetween(0, 2, inclusive: true); + check(2).isBetween(0, 2, inclusive: true); + }); + + test('isBetween fails outside range', () { + check(3).isRejectedBy( + (it) => it.isBetween(0, 2), + which: ['is not between <0>', 'and <2>', ' (inclusive)'], + ); + check(0).isRejectedBy( + (it) => it.isBetween(0, 2, inclusive: false), + which: ['is not between <0>', 'and <2>', ' (exclusive)'], + ); + }); + }); + + group('IntChecks', () { + test('isEven succeeds for even numbers', () { + check(2).isEven(); + check(0).isEven(); + check(-2).isEven(); + }); + + test('isEven fails for odd numbers', () { + check(1).isRejectedBy((it) => it.isEven(), which: ['is odd']); + }); + + test('isOdd succeeds for odd numbers', () { + check(1).isOdd(); + check(-1).isOdd(); + }); + + test('isOdd fails for even numbers', () { + check(2).isRejectedBy((it) => it.isOdd(), which: ['is even']); + }); + + test('isMultipleOf succeeds when divisible', () { + check(4).isMultipleOf(2); + check(0).isMultipleOf(5); + }); + + test('isMultipleOf fails when not divisible', () { + check(3).isRejectedBy( + (it) => it.isMultipleOf(2), + which: ['is not a multiple of <2>'], + ); + }); + + test('isDivisibleBy is alias for isMultipleOf', () { + check(4).isDivisibleBy(2); + check(3).isRejectedBy( + (it) => it.isDivisibleBy(2), + which: ['is not a multiple of <2>'], + ); + }); + }); +} diff --git a/checks_ext/test/core/regexp_test.dart b/checks_ext/test/core/regexp_test.dart new file mode 100644 index 00000000..b8fb2eb1 --- /dev/null +++ b/checks_ext/test/core/regexp_test.dart @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/regexp.dart'; +import '../util.dart'; + +void main() { + group('RegExpChecks', () { + test('properties extract correct values', () { + final regExp = RegExp(r'a', caseSensitive: false, multiLine: true); + check(regExp) + ..pattern.equals('a') + ..isCaseSensitive.isFalse() + ..isMultiLine.isTrue(); + }); + + test('firstMatch extracts first match', () { + final regExp = RegExp(r'a'); + check( + regExp, + ).firstMatch('banana').isNotNull().has((m) => m.start, 'start').equals(1); + }); + + test('allMatches extracts all matches', () { + final regExp = RegExp(r'a'); + check( + regExp, + ).allMatches('banana').has((it) => it.length, 'length').equals(3); + }); + + test('hasMatch succeeds when matching', () { + final regExp = RegExp(r'a'); + check(regExp).hasMatch('banana'); + }); + + test('hasMatch fails when not matching', () { + final regExp = RegExp(r'z'); + check(regExp).isRejectedBy( + (it) => it.hasMatch('banana'), + which: ["does not match 'banana'"], + ); + }); + + test('hasNoMatch succeeds when not matching', () { + final regExp = RegExp(r'z'); + check(regExp).hasNoMatch('banana'); + }); + + testCheckGolden( + () { + final regExp = RegExp(r'a'); + check(regExp).hasNoMatch('banana'); + }, + ''' +# hasNoMatch failure message golden +Expected: a RegExp that: + does not match 'banana' +Actual: +Which: matched 'a' at index 1: +banana + ^''', + ); + + testCheckGolden( + () { + final regExp = RegExp(r'a'); + check(regExp).hasNoMatch('line1\nline2 with a\nline3'); + }, + ''' +# hasNoMatch failure message golden with multiline +Expected: a RegExp that: + does not match 'line1 + line2 with a + line3' +Actual: +Which: matched 'a' at index 17: +line2 with a + ^''', + ); + }); +} diff --git a/checks_ext/test/core/regexpmatch_test.dart b/checks_ext/test/core/regexpmatch_test.dart new file mode 100644 index 00000000..9032d9b9 --- /dev/null +++ b/checks_ext/test/core/regexpmatch_test.dart @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/regexpmatch.dart'; +import '../util.dart'; + +void main() { + group('RegExpMatchChecks', () { + final regExp = RegExp(r'(?\w+) (?\d+)'); + const input = 'hello 42'; + final match = regExp.firstMatch(input)!; + + test('namedGroup extracts named group', () { + check(match) + ..namedGroup('word').equals('hello') + ..namedGroup('num').equals('42'); + }); + + test('groupNames returns names', () { + check(match).groupNames.deepEquals(['word', 'num']); + }); + + testCheckGolden( + () { + check(match).namedGroup('word').equals('world'); + }, + ''' +# namedGroup failure message golden +Expected: a RegExpMatch that: + has namedGroup(word) that: + equals 'world' +Actual: a RegExpMatch that: + has namedGroup(word) that: + Actual: 'hello' + Which: are not equal''', + ); + }); +} diff --git a/checks_ext/test/core/set_test.dart b/checks_ext/test/core/set_test.dart new file mode 100644 index 00000000..2fd300c0 --- /dev/null +++ b/checks_ext/test/core/set_test.dart @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/set.dart'; +import '../util.dart'; + +void main() { + group('SetChecks', () { + test('isSubsetOf succeeds when it is a subset', () { + check({1, 2}).isSubsetOf({1, 2, 3}); + }); + + test('isSubsetOf succeeds when equal', () { + check({1, 2}).isSubsetOf({1, 2}); + }); + + test('isSupersetOf succeeds when it is a superset', () { + check({1, 2, 3}).isSupersetOf({1, 2}); + }); + + test('isSupersetOf succeeds when equal', () { + check({1, 2}).isSupersetOf({1, 2}); + }); + + testCheckGolden( + () { + check({1, 2, 3}).isSubsetOf({1, 2}); + }, + ''' +# isSubsetOf failure message golden +Expected: a Set that: + is a subset of {1, 2} +Actual: {1, 2, 3} +Which: contains elements not in the other set: {3}''', + ); + + testCheckGolden( + () { + check({1, 2}).isSupersetOf({1, 2, 3}); + }, + ''' +# isSupersetOf failure message golden +Expected: a Set that: + is a superset of {1, 2, 3} +Actual: {1, 2} +Which: is missing elements from the other set: {3}''', + ); + }); +} diff --git a/checks_ext/test/core/stopwatch_test.dart b/checks_ext/test/core/stopwatch_test.dart new file mode 100644 index 00000000..b9478f0f --- /dev/null +++ b/checks_ext/test/core/stopwatch_test.dart @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/stopwatch.dart'; +import '../util.dart'; + +void main() { + group('StopwatchChecks', () { + test('properties extract correct values', () { + final stopwatch = Stopwatch(); + check(stopwatch) + ..isNotRunning() + ..elapsed.equals(Duration.zero) + ..elapsedTicks.equals(0) + ..elapsedMilliseconds.equals(0); + }); + + test('reflects running state', () { + final stopwatch = Stopwatch()..start(); + check(stopwatch).isRunning(); + stopwatch.stop(); + check(stopwatch).isNotRunning(); + }); + + test('elapsed increases when running', () async { + final stopwatch = Stopwatch()..start(); + // A short sleep to ensure some time elapses + await Future.delayed(const Duration(milliseconds: 5)); + stopwatch.stop(); + + check(stopwatch).elapsed.isGreaterThan(Duration.zero); + check(stopwatch).elapsedMilliseconds.isGreaterThan(0); + }); + testCheckGolden( + () { + final Stopwatch stopwatch = FakeStopwatch( + isRunning: false, + elapsed: Duration.zero, + ); + check(stopwatch).isRunning(); + }, + ''' +# isRunning failure message golden +Expected: a Stopwatch that: + is running +Actual: Stopwatch(running: false, elapsed: 0s) +Which: is not running''', + ); + + testCheckGolden( + () { + final Stopwatch stopwatch = FakeStopwatch( + isRunning: true, + elapsed: const Duration(seconds: 5), + ); + check(stopwatch).isNotRunning(); + }, + ''' +# isNotRunning failure message golden +Expected: a Stopwatch that: + is not running +Actual: Stopwatch(running: true, elapsed: 5s) +Which: is running''', + ); + }); +} + +class FakeStopwatch implements Stopwatch { + @override + final bool isRunning; + @override + final Duration elapsed; + + FakeStopwatch({required this.isRunning, required this.elapsed}); + + @override + int get elapsedTicks => elapsed.inMicroseconds; + @override + int get elapsedMicroseconds => elapsed.inMicroseconds; + @override + int get elapsedMilliseconds => elapsed.inMilliseconds; + @override + int get frequency => 1000000; + + @override + void start() {} + @override + void stop() {} + @override + void reset() {} +} diff --git a/checks_ext/test/core/string_test.dart b/checks_ext/test/core/string_test.dart new file mode 100644 index 00000000..4f322282 --- /dev/null +++ b/checks_ext/test/core/string_test.dart @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/string.dart'; +import '../util.dart'; + +void main() { + group('StringChecks', () { + test('isBlank succeeds when empty or whitespace', () { + check('').isBlank(); + check(' ').isBlank(); + }); + + test('isBlank fails when not blank', () { + check( + ' a ', + ).isRejectedBy((it) => it.isBlank(), which: ['was not blank']); + }); + + test('isNotBlank succeeds when not blank', () { + check(' a ').isNotBlank(); + }); + + test('isNotBlank fails when blank', () { + check(' ').isRejectedBy((it) => it.isNotBlank(), which: ['was blank']); + }); + + test('lines extracts lines', () { + check('a\nb\nc').lines.deepEquals(['a', 'b', 'c']); + }); + + test('isLowerCase succeeds when all lower case', () { + check('abc').isLowerCase(); + check('abc1').isLowerCase(); + }); + + test('isLowerCase fails when contains upper case', () { + check( + 'Abc', + ).isRejectedBy((it) => it.isLowerCase(), which: ['was not lower case']); + }); + + test('isUpperCase succeeds when all upper case', () { + check('ABC').isUpperCase(); + check('ABC1').isUpperCase(); + }); + + test('isUpperCase fails when contains lower case', () { + check( + 'Abc', + ).isRejectedBy((it) => it.isUpperCase(), which: ['was not upper case']); + }); + + test('isCapitalized succeeds when starts with upper case', () { + check('Abc').isCapitalized(); + check('A').isCapitalized(); + }); + + test('isCapitalized fails when starts with lower case or empty', () { + check('abc').isRejectedBy( + (it) => it.isCapitalized(), + which: ['first character was not upper case'], + ); + check('').isRejectedBy((it) => it.isCapitalized(), which: ['is empty']); + }); + + test('isNumeric succeeds when only digits', () { + check('123').isNumeric(); + }); + + test('isNumeric fails when contains non-digits', () { + check('12a').isRejectedBy( + (it) => it.isNumeric(), + which: ['contains non-numeric characters'], + ); + }); + + test('isAlpha succeeds when only letters', () { + check('abc').isAlpha(); + check('ABC').isAlpha(); + }); + + test('isAlpha fails when contains non-letters', () { + check('abc1').isRejectedBy( + (it) => it.isAlpha(), + which: ['contains non-alphabetic characters'], + ); + }); + + test('isAlphanumeric succeeds when only letters and digits', () { + check('abc1').isAlphanumeric(); + check('ABC123').isAlphanumeric(); + }); + + test('isAlphanumeric fails when contains other characters', () { + check('abc_1').isRejectedBy( + (it) => it.isAlphanumeric(), + which: ['contains characters that are not letters or digits'], + ); + }); + + test('containsIgnoringCase succeeds', () { + check('Hello World').containsIgnoringCase('world'); + check('Hello World').containsIgnoringCase('WORLD'); + check('Hello World').containsIgnoringCase('hello'); + }); + + test('containsIgnoringCase fails', () { + check('Hello World').isRejectedBy( + (it) => it.containsIgnoringCase('foo'), + which: ["does not contain 'foo' (case-insensitive)"], + ); + }); + }); +} diff --git a/checks_ext/test/core/stringbuffer_test.dart b/checks_ext/test/core/stringbuffer_test.dart new file mode 100644 index 00000000..acecf998 --- /dev/null +++ b/checks_ext/test/core/stringbuffer_test.dart @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/stringbuffer.dart'; +import '../util.dart'; + +void main() { + group('StringBufferChecks', () { + test('properties extract correct values', () { + final buffer = StringBuffer('hello'); + check(buffer) + ..asString.equals('hello') + ..length.equals(5) + ..isNotEmpty(); + }); + + test('isEmpty succeeds when empty', () { + final buffer = StringBuffer(); + check(buffer).isEmpty(); + }); + + test('isNotEmpty succeeds when not empty', () { + final buffer = StringBuffer('hello'); + check(buffer).isNotEmpty(); + }); + + testCheckGolden( + () { + final buffer = StringBuffer('hello'); + check(buffer).isEmpty(); + }, + ''' +# isEmpty failure message golden +Expected: a StringBuffer that: + is empty +Actual: +Which: is not empty''', + ); + + testCheckGolden( + () { + final buffer = StringBuffer(); + check(buffer).isNotEmpty(); + }, + ''' +# isNotEmpty failure message golden +Expected: a StringBuffer that: + is not empty +Which: is empty''', + ); + }); +} diff --git a/checks_ext/test/core/uri_test.dart b/checks_ext/test/core/uri_test.dart new file mode 100644 index 00000000..afa5fc98 --- /dev/null +++ b/checks_ext/test/core/uri_test.dart @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/uri.dart'; +import '../util.dart'; + +void main() { + group('UriChecks', () { + test('properties extract correct values', () { + final uri = Uri.parse( + 'https://example.com:8080/path/to/res?id=42#fragment', + ); + check(uri) + ..scheme.equals('https') + ..authority.equals('example.com:8080') + ..host.equals('example.com') + ..port.equals(8080) + ..path.equals('/path/to/res') + ..query.equals('id=42') + ..fragment.equals('fragment') + ..pathSegments.deepEquals(['path', 'to', 'res']) + ..queryParameters.deepEquals({'id': '42'}) + ..queryParametersAll.deepEquals({ + 'id': ['42'], + }); + }); + + test('isAbsolute succeeds when absolute', () { + final uri = Uri.parse('https://example.com'); + check(uri).isAbsolute(); + }); + + test('isAbsolute fails when relative', () { + final uri = Uri.parse('/path'); + check( + uri, + ).isRejectedBy((it) => it.isAbsolute(), which: ['is not absolute']); + }); + + test('hasScheme succeeds when matching', () { + final uri = Uri.parse('https://example.com'); + check(uri).hasScheme('https'); + }); + + test('hasScheme fails when not matching', () { + final uri = Uri.parse('https://example.com'); + check(uri).isRejectedBy( + (it) => it.hasScheme('http'), + which: ["has scheme 'https'"], + ); + }); + testCheckGolden( + () { + final uri = Uri.parse('/path'); + check(uri).isAbsolute(); + }, + ''' +# isAbsolute failure message golden +Expected: a Uri that: + is absolute +Actual: +Which: is not absolute''', + ); + + testCheckGolden( + () { + final uri = Uri.parse('https://example.com'); + check(uri).isNotAbsolute(); + }, + ''' +# isNotAbsolute failure message golden +Expected: a Uri that: + is not absolute +Actual: +Which: is absolute''', + ); + }); +} diff --git a/checks_ext/test/core/uridata_test.dart b/checks_ext/test/core/uridata_test.dart new file mode 100644 index 00000000..4062b489 --- /dev/null +++ b/checks_ext/test/core/uridata_test.dart @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/core/uridata.dart'; +import '../util.dart'; + +void main() { + group('UriDataChecks', () { + test('properties extract correct values', () { + final data = UriData.parse( + 'data:text/plain;charset=utf-8;base64,aGVsbG8=', + ); + check(data) + ..mimeType.equals('text/plain') + ..charset.equals('utf-8') + ..isBase64() + ..contentText.equals('aGVsbG8=') + ..contentAsString.equals('hello') + ..contentAsBytes.deepEquals([ + 104, + 101, + 108, + 108, + 111, + ]); // 'hello' in ASCII + }); + + test('isBase64 succeeds when base64', () { + final data = UriData.parse('data:text/plain;base64,aGVsbG8='); + check(data).isBase64(); + }); + + test('isBase64 fails when not base64', () { + final data = UriData.parse('data:text/plain,hello'); + check(data).isRejectedBy((it) => it.isBase64(), which: ['is not base64']); + }); + }); +} diff --git a/checks_ext/test/examples_test.dart b/checks_ext/test/examples_test.dart new file mode 100644 index 00000000..23442f49 --- /dev/null +++ b/checks_ext/test/examples_test.dart @@ -0,0 +1,32 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// This library tests that the examples in the `example/` directory +/// produce the output described in their comments. +/// +/// Recipe for adding a new example: +/// 1. Create a file `example/your_feature.dart`. +/// 2. Add a `main()` function that demonstrates success and failure. +/// 3. Document the expected output using `//` comments. +/// Lines starting with `///` are treated as documentation and ignored. +/// 4. In the documentation for the feature, add the directive: +/// `/// {@example /example/your_feature.dart}` +/// +/// The test extracts the expected output from the comments (lines starting with `//` but not `///`) +/// and verifies that running the example as a standalone script produces that output in stdout or stderr. +library; + +import 'check_examples.dart'; + +void main() => checkExamples(); diff --git a/checks_ext/test/math/point_test.dart b/checks_ext/test/math/point_test.dart new file mode 100644 index 00000000..a3fcc538 --- /dev/null +++ b/checks_ext/test/math/point_test.dart @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:checks_ext/src/math/point.dart'; +import '../util.dart'; + +void main() { + group('PointChecks', () { + test('properties extract correct values', () { + final point = Point(3, 4); + check(point) + ..x.equals(3) + ..y.equals(4) + ..magnitude.equals(5.0); + }); + }); +} diff --git a/checks_ext/test/math/rectangle_test.dart b/checks_ext/test/math/rectangle_test.dart new file mode 100644 index 00000000..4693f9de --- /dev/null +++ b/checks_ext/test/math/rectangle_test.dart @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:checks_ext/src/math/rectangle.dart'; +import '../util.dart'; + +void main() { + group('RectangleChecks', () { + test('properties extract correct values', () { + final rectangle = Rectangle(0, 0, 10, 20); + check(rectangle) + ..left.equals(0) + ..top.equals(0) + ..width.equals(10) + ..height.equals(20) + ..right.equals(10) + ..bottom.equals(20); + }); + + test('containsPoint succeeds when inside', () { + final rectangle = Rectangle(0, 0, 10, 20); + check(rectangle).containsPoint(Point(5, 5)); + }); + + testCheckGolden( + () { + final rectangle = Rectangle(0, 0, 10, 20); + check(rectangle).containsPoint(Point(15, 5)); + }, + ''' +# containsPoint failure message golden +Expected: a Rectangle that: + contains point +Actual: +Which: does not contain point ''', + ); + test('containsRectangle succeeds when inside', () { + final rectangle = Rectangle(0, 0, 10, 20); + check(rectangle).containsRectangle(Rectangle(2, 2, 5, 5)); + }); + + testCheckGolden( + () { + final rectangle = Rectangle(0, 0, 10, 20); + check(rectangle).containsRectangle(Rectangle(5, 5, 10, 10)); + }, + ''' +# containsRectangle failure message golden +Expected: a Rectangle that: + contains rectangle +Actual: +Which: does not contain rectangle ''', + ); + }); +} diff --git a/checks_ext/test/typed_data/bytebuffer_test.dart b/checks_ext/test/typed_data/bytebuffer_test.dart new file mode 100644 index 00000000..2644ee8e --- /dev/null +++ b/checks_ext/test/typed_data/bytebuffer_test.dart @@ -0,0 +1,44 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:checks_ext/src/typed_data/bytebuffer.dart'; +import '../util.dart'; + +void main() { + group('ByteBufferChecks', () { + test('properties extract correct values', () { + final list = Uint8List.fromList([1, 2, 3, 4]); + final buffer = list.buffer; + + check(buffer) + ..lengthInBytes.equals(4) + ..asUint8List().deepEquals([1, 2, 3, 4]); + }); + + test('asUint8List with offset and length', () { + final list = Uint8List.fromList([1, 2, 3, 4]); + final buffer = list.buffer; + + check(buffer).asUint8List(1, 2).deepEquals([2, 3]); + }); + + test('asInt8List extracts correct values', () { + final list = Int8List.fromList([-1, -2, 3, 4]); + final buffer = list.buffer; + + check(buffer).asInt8List().deepEquals([-1, -2, 3, 4]); + }); + }); +} diff --git a/checks_ext/test/typed_data/bytedata_test.dart b/checks_ext/test/typed_data/bytedata_test.dart new file mode 100644 index 00000000..bdcc2e2f --- /dev/null +++ b/checks_ext/test/typed_data/bytedata_test.dart @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:checks_ext/src/typed_data/bytedata.dart'; +import '../util.dart'; + +void main() { + group('ByteDataChecks', () { + test('properties extract correct values', () { + final data = ByteData(10); + check(data).lengthInBytes.equals(10); + }); + + test('float32At extracts correct value', () { + final data = ByteData(4); + data.setFloat32(0, 1.5, Endian.little); + check(data).float32At(0).equals(1.5); + + data.setFloat32(0, 1.5, Endian.big); + check(data).float32At(0, Endian.big).equals(1.5); + }); + + test('uint32At extracts correct value', () { + final data = ByteData(4); + data.setUint32(0, 42, Endian.little); + check(data).uint32At(0).equals(42); + }); + + test('uint16At extracts correct value', () { + final data = ByteData(2); + data.setUint16(0, 42, Endian.little); + check(data).uint16At(0).equals(42); + }); + + test('uint8At extracts correct value', () { + final data = ByteData(1); + data.setUint8(0, 42); + check(data).uint8At(0).equals(42); + }); + + test('respects offsetInBytes', () { + final buffer = Uint8List(10).buffer; + final data = ByteData.view(buffer, 2, 4); // Offset 2, length 4 + data.setUint8(0, 42); // Sets byte at index 2 in buffer + + check(data).uint8At(0).equals(42); + }); + }); +} diff --git a/checks_ext/test/typed_data/bytesbuilder_test.dart b/checks_ext/test/typed_data/bytesbuilder_test.dart new file mode 100644 index 00000000..c1c53466 --- /dev/null +++ b/checks_ext/test/typed_data/bytesbuilder_test.dart @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:checks_ext/src/typed_data/bytesbuilder.dart'; +import '../util.dart'; + +void main() { + group('BytesBuilderChecks', () { + test('properties extract correct values', () { + final builder = BytesBuilder() + ..add([1, 2]) + ..addByte(3); + + check(builder) + ..length.equals(3) + ..bytes.deepEquals([1, 2, 3]) + ..isNotEmpty(); + }); + + test('isEmpty succeeds when empty', () { + final builder = BytesBuilder(); + check(builder).isEmpty(); + }); + + test('isNotEmpty fails when empty', () { + final builder = BytesBuilder(); + check(builder).isRejectedBy((it) => it.isNotEmpty(), which: ['is empty']); + }); + + test('isEmpty fails when not empty', () { + final builder = BytesBuilder()..addByte(1); + check( + builder, + ).isRejectedBy((it) => it.isEmpty(), which: ['is not empty']); + }); + }); +} diff --git a/checks_ext/test/typed_data/simd_test.dart b/checks_ext/test/typed_data/simd_test.dart new file mode 100644 index 00000000..9a5be9b8 --- /dev/null +++ b/checks_ext/test/typed_data/simd_test.dart @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:checks_ext/src/typed_data/simd.dart'; +import '../util.dart'; + +void main() { + group('Float32x4Checks', () { + test('properties extract correct values', () { + final v = Float32x4(1.0, 2.0, 3.0, 4.0); + check(v) + ..x.equals(1.0) + ..y.equals(2.0) + ..z.equals(3.0) + ..w.equals(4.0); + }); + }); + + group('Float64x2Checks', () { + test('properties extract correct values', () { + final v = Float64x2(1.0, 2.0); + check(v) + ..x.equals(1.0) + ..y.equals(2.0); + }); + }); + + group('Int32x4Checks', () { + test('properties extract correct values', () { + final v = Int32x4(1, 2, 3, 4); + check(v) + ..x.equals(1) + ..y.equals(2) + ..z.equals(3) + ..w.equals(4); + }); + }); +} diff --git a/checks_ext/test/typed_data/typeddata_test.dart b/checks_ext/test/typed_data/typeddata_test.dart new file mode 100644 index 00000000..9daf12da --- /dev/null +++ b/checks_ext/test/typed_data/typeddata_test.dart @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:checks_ext/src/typed_data/typeddata.dart'; +import '../util.dart'; + +void main() { + group('TypedDataChecks', () { + + + test('asByteData extracts correct view respecting offset', () { + final buffer = Uint8List.fromList([0, 0, 1, 2, 3, 4, 0, 0]).buffer; + final data = ByteData.view(buffer, 2, 4); // Offset 2, length 4 + + check(data).asByteData.has((b) => b.lengthInBytes, 'lengthInBytes').equals(4); + check(data).asByteData.has((b) => b.getUint8(0), 'first byte').equals(1); + }); + }); +} diff --git a/checks_ext/test/util.dart b/checks_ext/test/util.dart new file mode 100644 index 00000000..cd5e063d --- /dev/null +++ b/checks_ext/test/util.dart @@ -0,0 +1,161 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Guidelines for testing checks extensions: +/// +/// 1. **Hybrid Approach for Failure Messages**: +/// - Use `isRejectedBy` for the bulk of tests (empty, one element, multiple) +/// to test the logic and raw rejection data (`which` and `actual`). +/// - Use `throwsFailure().contains('label')` to verify that specific labels +/// (like "has sum") appear in the full failure message, without hardcoding +/// the entire layout which is fragile to core `checks` changes. +/// +/// 2. **Test Variants**: +/// - Always test positive cases (succeeds). +/// - Always test negative cases (fails with correct rejection). +/// - Test empty or null cases when applicable. +/// - Test with different types if supported (e.g. numbers, letters). + +import 'dart:io'; +import 'package:checks/checks.dart'; +import 'package:checks/context.dart'; +import 'package:stack_trace/stack_trace.dart'; + +export 'package:checks/checks.dart'; +export 'package:checks/context.dart'; +import 'package:test/test.dart' show test, markTestSkipped, TestFailure; +export 'package:test/test.dart' hide escape, throws; + +// Copied from checks/test/test_shared.dart +extension RejectionChecks on Subject { + void isRejectedBy( + Condition condition, { + Iterable? actual, + Iterable? which, + }) { + late T actualValue; + var didRunCallback = false; + final rejection = context.nest( + () => ['does not meet a condition with a Rejection'], + (value) { + actualValue = value; + didRunCallback = true; + final failure = softCheck(value, condition); + if (failure == null) { + return Extracted.rejection( + which: [ + 'was accepted by the condition checking:', + ...describe(condition), + ], + ); + } + return Extracted.value(failure.rejection); + }, + ); + if (didRunCallback) { + rejection + .has((r) => r.actual, 'actual') + .deepEquals(actual ?? literal(actualValue)); + } else { + rejection + .has((r) => r.actual, 'actual') + .context + .expect(() => ['is left default'], (_) => null); + } + if (which == null) { + rejection.has((r) => r.which, 'which').isNull(); + } else { + rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); + } + } +} + +// Copied from checks/test/failure_message_test.dart +extension FailureMessageChecks on Subject { + Subject throwsFailure() => + throws().has((f) => f.message, 'message').isNotNull(); +} + +/// Runs a test with a golden failure message. +/// +/// The [expected] string must start with `# ` on the first line. +/// If the failure message doesn't match and `UPDATE_GOLDEN` environment variable +/// is set to `true`, it will update the source file with the actual message. +void testCheckGolden(void Function() callback, String expected) { + final trace = Trace.current(); + final lines = expected.split('\n'); + if (lines.isEmpty || !lines.first.startsWith('# ')) { + throw ArgumentError('Expected string must start with "# <title>"'); + } + final title = lines.first.substring(2).trim(); + + test(title, () { + String? failureMessage; + try { + callback(); + } on TestFailure catch (e) { + failureMessage = e.message; + } + + if (failureMessage == null) { + throw TestFailure('Expected a failure but it passed.'); + } + + final actual = '# $title\n$failureMessage'; + final expectedTrimmed = expected.trim(); + final actualTrimmed = actual.trim(); + + if (actualTrimmed != expectedTrimmed) { + final updateGolden = Platform.environment['UPDATE_GOLDEN'] == 'true'; + if (updateGolden) { + Frame? callerFrame; + for (final frame in trace.frames) { + final path = frame.uri.toString(); + if (path.contains('test/') && !path.contains('util.dart')) { + callerFrame = frame; + break; + } + } + if (callerFrame == null) { + throw StateError('Cannot find caller file in stack trace'); + } + final file = File(callerFrame.uri.toFilePath()); + final content = file.readAsStringSync(); + + final matches = _countMatches(content, expected); + if (matches != 1) { + throw TestFailure( + 'Cannot update golden: found $matches occurrences of the expected string in ${file.path}. It must appear exactly once.', + ); + } + + final newContent = content.replaceFirst(expected, actual); + file.writeAsStringSync(newContent); + markTestSkipped('Golden updated!'); + } else { + check(actualTrimmed).equals(expectedTrimmed); + } + } + }); +} + +int _countMatches(String source, String target) { + var count = 0; + var index = 0; + while ((index = source.indexOf(target, index)) != -1) { + count++; + index += target.length; + } + return count; +} diff --git a/checks_ext/test/util_test.dart b/checks_ext/test/util_test.dart new file mode 100644 index 00000000..b135c4c2 --- /dev/null +++ b/checks_ext/test/util_test.dart @@ -0,0 +1,94 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:checks_ext/src/util.dart'; +import 'util.dart'; + +void main() { + group('ExpectExtension', () { + const desc = (positive: 'is positive', negative: 'is negative'); + + group('expectTrue', () { + test('succeeds when predicate returns true', () { + check(1).expectTrue(desc, (x) => x > 0); + }); + + test('fails when predicate returns false', () { + check(-1).isRejectedBy( + (it) => it.expectTrue(desc, (x) => x > 0), + which: ['is negative'], + ); + }); + }); + + group('expectFalse', () { + test('succeeds when predicate returns false', () { + check(-1).expectFalse(desc, (x) => x > 0); + }); + + test('fails when predicate returns true', () { + check(1).isRejectedBy( + (it) => it.expectFalse(desc, (x) => x > 0), + which: ['is positive'], + ); + }); + + testCheckGolden( + () { + check(1).expectFalse(desc, (x) => x > 0); + }, + ''' +# expectFalse failure message golden +Expected: a int that: + is negative +Actual: <1> +Which: is positive''', + ); + }); + }); + + group('prettyPrintDuration', () { + test('formats zero', () { + check(prettyPrintDuration(Duration.zero)).equals('0s'); + }); + + test('formats days and hours', () { + check( + prettyPrintDuration(Duration(days: 3, hours: 5)), + ).equals('3 days 5 hours'); + }); + + test('formats minutes and seconds', () { + check( + prettyPrintDuration(Duration(minutes: 5, seconds: 30)), + ).equals('5m 30s'); + }); + + test('formats milliseconds and microseconds', () { + check( + prettyPrintDuration(Duration(milliseconds: 500, microseconds: 500)), + ).equals('500ms 500us'); + }); + + test('formats negative duration', () { + check(prettyPrintDuration(Duration(seconds: -30))).equals('-30s'); + }); + + test('omits zero components', () { + check( + prettyPrintDuration(Duration(days: 1, seconds: 30)), + ).equals('1 day 30s'); + }); + }); +}