diff --git a/CHANGELOG.md b/CHANGELOG.md index f60dc45..f8d5b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.9.32 + +- Tests: + - Expanded coverage of `bones_api_utils_collections`: + - `isEqualsDeep` / `isEqualsListDeep` / `isEqualsIterableDeep` / `isEqualsSetDeep` / `isEqualsMapDeep` (including nested structures, `null`/identity edge cases and custom `valueEquality`). + - `intersectsIterableDeep`. + - `deepCopy` / `deepCopyList` / `deepCopySet` / `deepCopyMap` (typed fast paths, `Uint8List`/`Int8List`, and copy independence). + - `MapAsCacheExtension` (`getCached`, `getCachedNullable`, `getCachedAsync`, `getCachedAsyncNullable`, `getIfCached`, `checkCacheLimit`). + - `MapOfCachesExtension` (`getMultiCached*`, `isEquivalentContext` wildcard semantics, `equivalentCaches`). + - `RecordExtension.positionalParametersLength`. + - Expanded coverage of `Arguments`: case-sensitive keys, trailing single-dash flags, the trailing double-dash error path, `keysAbbreviations` and `toArgumentsList`. + ## 1.9.31 - Bug fixes: diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index de2336e..be12462 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -48,7 +48,7 @@ typedef APILogger = /// Bones API Library class. class BonesAPI { // ignore: constant_identifier_names - static const String VERSION = '1.9.31'; + static const String VERSION = '1.9.32'; static bool _boot = false; diff --git a/pubspec.yaml b/pubspec.yaml index b06f3f6..d537d47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - A powerful API backend framework for Dart. It comes with a built-in HTTP Server, route handler, entity handler, SQL translator, and DB adapters. -version: 1.9.31 +version: 1.9.32 homepage: https://github.com/Colossus-Services/bones_api environment: diff --git a/test/bones_api_arguments_test.dart b/test/bones_api_arguments_test.dart index ef7ebf5..9b6df84 100644 --- a/test/bones_api_arguments_test.dart +++ b/test/bones_api_arguments_test.dart @@ -86,5 +86,40 @@ void main() { equals('-v --address host --port 80 --list 1 --list 2 --list 3'), ); }); + + test('caseSensitive keys', () async { + var argsInsensitive = Arguments.parseLine('-Address host'); + expect(argsInsensitive.parameters, equals({'address': 'host'})); + + var argsSensitive = Arguments.parseLine( + '-Address host', + caseSensitive: true, + ); + expect(argsSensitive.parameters, equals({'Address': 'host'})); + }); + + test('trailing single-dash key becomes a flag', () async { + var args = Arguments.parseLine('x -v'); + expect(args.flags, equals({'v'})); + expect(args.parameters, isEmpty); + expect(args.args, equals(['x'])); + }); + + test('trailing double-dash parameter with no value throws', () async { + expect(() => Arguments.parseLine('x --foo'), throwsA(isA())); + }); + + test('keysAbbreviations inverts abbreviations', () async { + var args = Arguments.parseLine( + '-a host', + abbreviations: {'a': 'address', 'v': 'verbose'}, + ); + expect(args.keysAbbreviations, equals({'address': 'a', 'verbose': 'v'})); + }); + + test('toArgumentsList', () async { + var args = Arguments.parseLine('x -a 1 -f'); + expect(args.toArgumentsList(), equals(['-f', '--a', '1'])); + }); }); } diff --git a/test/bones_api_utils_collection_test.dart b/test/bones_api_utils_collection_test.dart index 6da583a..63db173 100644 --- a/test/bones_api_utils_collection_test.dart +++ b/test/bones_api_utils_collection_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bones_api/bones_api.dart'; import 'package:test/test.dart'; @@ -78,4 +80,461 @@ void main() { expect(s1.toList(), unorderedEquals(['b'])); }); }); + + group('isEqualsDeep', () { + test('primitives and identity', () { + expect(isEqualsDeep(1, 1), isTrue); + expect(isEqualsDeep('a', 'a'), isTrue); + expect(isEqualsDeep(1, 2), isFalse); + expect(isEqualsDeep(null, null), isTrue); + expect(isEqualsDeep(null, 1), isFalse); + expect(isEqualsDeep(1, null), isFalse); + + var o = Object(); + expect(isEqualsDeep(o, o), isTrue); + }); + + test('lists', () { + expect(isEqualsListDeep([1, 2, 3], [1, 2, 3]), isTrue); + expect(isEqualsListDeep([1, 2, 3], [1, 2, 4]), isFalse); + expect(isEqualsListDeep([1, 2], [1, 2, 3]), isFalse); + expect(isEqualsListDeep(null, [1]), isFalse); + expect(isEqualsListDeep([1], null), isFalse); + expect(isEqualsListDeep(null, null), isTrue); + + var l = [1, 2]; + expect(isEqualsListDeep(l, l), isTrue); + + // nested + expect( + isEqualsDeep( + [ + [1, 2], + [3, 4], + ], + [ + [1, 2], + [3, 4], + ], + ), + isTrue, + ); + expect( + isEqualsDeep( + [ + [1, 2], + ], + [ + [1, 9], + ], + ), + isFalse, + ); + }); + + test('iterables', () { + expect(isEqualsIterableDeep([1, 2].map((e) => e), [1, 2]), isTrue); + expect(isEqualsIterableDeep([1, 2].where((e) => true), [1, 3]), isFalse); + expect(isEqualsIterableDeep([1].map((e) => e), [1, 2]), isFalse); + expect(isEqualsIterableDeep(null, [1]), isFalse); + expect(isEqualsIterableDeep([1], null), isFalse); + expect(isEqualsIterableDeep(null, null), isTrue); + }); + + test('sets', () { + expect(isEqualsSetDeep({1, 2, 3}, {3, 2, 1}), isTrue); + expect(isEqualsSetDeep({1, 2}, {1, 2, 3}), isFalse); + expect(isEqualsSetDeep({1, 2}, {1, 9}), isFalse); + expect(isEqualsSetDeep(null, {1}), isFalse); + expect(isEqualsSetDeep({1}, null), isFalse); + expect(isEqualsSetDeep(null, null), isTrue); + + var s = {1, 2}; + expect(isEqualsSetDeep(s, s), isTrue); + }); + + test('maps', () { + expect(isEqualsMapDeep({'a': 1, 'b': 2}, {'b': 2, 'a': 1}), isTrue); + expect(isEqualsMapDeep({'a': 1}, {'a': 2}), isFalse); + expect(isEqualsMapDeep({'a': 1}, {'a': 1, 'b': 2}), isFalse); + expect(isEqualsMapDeep({'a': 1}, {'b': 1}), isFalse); + expect(isEqualsMapDeep(null, {'a': 1}), isFalse); + expect(isEqualsMapDeep({'a': 1}, null), isFalse); + expect(isEqualsMapDeep(null, null), isTrue); + + var m = {'a': 1}; + expect(isEqualsMapDeep(m, m), isTrue); + + // nested via isEqualsDeep dispatch + expect( + isEqualsDeep( + { + 'a': [1, 2], + }, + { + 'a': [1, 2], + }, + ), + isTrue, + ); + }); + + test('dispatch via isEqualsDeep', () { + expect(isEqualsDeep([1, 2], [1, 2]), isTrue); + expect(isEqualsDeep({1, 2}, {2, 1}), isTrue); + expect(isEqualsDeep({'a': 1}, {'a': 1}), isTrue); + // List vs Set -> not equal + expect(isEqualsDeep([1, 2], {1, 2}), isFalse); + }); + + test('valueEquality', () { + bool ciEquals(Object? a, Object? b) => + a.toString().toLowerCase() == b.toString().toLowerCase(); + + expect(isEqualsDeep('ABC', 'abc'), isFalse); + expect(isEqualsDeep('ABC', 'abc', valueEquality: ciEquals), isTrue); + expect(isEqualsDeep(['ABC'], ['abc'], valueEquality: ciEquals), isTrue); + }); + }); + + group('intersectsIterableDeep', () { + test('basic', () { + expect(intersectsIterableDeep([1, 2, 3], [4, 5, 2]), isTrue); + expect(intersectsIterableDeep([1, 2, 3], [4, 5, 6]), isFalse); + expect(intersectsIterableDeep([], [1]), isFalse); + expect(intersectsIterableDeep([1], []), isFalse); + expect(intersectsIterableDeep(null, [1]), isFalse); + expect(intersectsIterableDeep([1], null), isFalse); + + var l = [1, 2]; + expect(intersectsIterableDeep(l, l), isTrue); + }); + + test('deep elements', () { + expect( + intersectsIterableDeep( + [ + [1, 2], + ], + [ + [9, 9], + [1, 2], + ], + ), + isTrue, + ); + }); + }); + + group('deepCopy', () { + test('primitives', () { + expect(deepCopy(null), isNull); + expect(deepCopy('abc'), equals('abc')); + expect(deepCopy(42), equals(42)); + expect(deepCopy(3.14), equals(3.14)); + expect(deepCopy(true), equals(true)); + }); + + test('list copies are independent', () { + List original = [ + [1, 2], + [3, 4], + ]; + var copy = deepCopy(original) as List; + expect(copy, equals(original)); + expect(identical(copy, original), isFalse); + expect(identical(copy[0], original[0]), isFalse); + + (copy[0] as List)[0] = 99; + expect((original[0] as List)[0], equals(1)); + }); + + test('deepCopyList typed fast paths', () { + expect(deepCopyList(null), isNull); + expect(deepCopyList([]), equals([])); + expect(deepCopyList(['a', 'b']), equals(['a', 'b'])); + expect(deepCopyList([1, 2]), equals([1, 2])); + expect(deepCopyList([1.0, 2.0]), equals([1.0, 2.0])); + expect(deepCopyList([1, 2.0]), equals([1, 2.0])); + expect(deepCopyList([true, false]), equals([true, false])); + + var bytes = Uint8List.fromList([1, 2, 3]); + var bytesCopy = deepCopyList(bytes)!; + expect(bytesCopy, equals([1, 2, 3])); + expect(identical(bytesCopy, bytes), isFalse); + + var i8 = Int8List.fromList([1, -2, 3]); + var i8Copy = deepCopyList(i8)!; + expect(i8Copy, equals([1, -2, 3])); + expect(identical(i8Copy, i8), isFalse); + }); + + test('deepCopySet typed fast paths', () { + expect(deepCopySet(null), isNull); + expect(deepCopySet({}), equals({})); + expect(deepCopySet({'a'}), equals({'a'})); + expect(deepCopySet({1, 2}), equals({1, 2})); + expect(deepCopySet({1.0}), equals({1.0})); + expect(deepCopySet({1, 2.0}), equals({1, 2.0})); + expect(deepCopySet({true}), equals({true})); + + var original = >{ + [1, 2], + }; + var copy = deepCopySet(original)!; + expect(copy.first, equals([1, 2])); + expect(identical(copy.first, original.first), isFalse); + }); + + test('deepCopyMap typed fast paths', () { + expect(deepCopyMap(null), isNull); + expect(deepCopyMap({}), equals({})); + expect(deepCopyMap({'a': 'b'}), equals({'a': 'b'})); + expect(deepCopyMap({'a': 1}), equals({'a': 1})); + expect(deepCopyMap({'a': 1.0}), equals({'a': 1.0})); + expect(deepCopyMap({'a': 1}), equals({'a': 1})); + expect(deepCopyMap({'a': true}), equals({'a': true})); + + var original = >{ + 'k': [1, 2], + }; + var copy = deepCopyMap(original)!; + expect(copy['k'], equals([1, 2])); + expect(identical(copy['k'], original['k']), isFalse); + }); + + test('deepCopy dispatch for Set/Iterable/Map', () { + var s = deepCopy({1, 2})!; + expect(s, equals({1, 2})); + + var it = deepCopy([1, 2, 3].map((e) => e * 2))!; + expect(it, equals([2, 4, 6])); + + var m = deepCopy({'a': 1})!; + expect(m, equals({'a': 1})); + }); + }); + + group('MapAsCacheExtension', () { + test('getCached caches the computed value', () { + var cache = {}; + var calls = 0; + + var v1 = cache.getCached('a', () { + calls++; + return 10; + }); + expect(v1, equals(10)); + + var v2 = cache.getCached('a', () { + calls++; + return 20; + }); + expect(v2, equals(10)); + expect(calls, equals(1)); + }); + + test('getCachedNullable does not cache null', () { + var cache = {}; + var calls = 0; + + var v = cache.getCachedNullable('a', () { + calls++; + return null; + }); + expect(v, isNull); + expect(cache.containsKey('a'), isFalse); + + var v2 = cache.getCachedNullable('a', () { + calls++; + return 5; + }); + expect(v2, equals(5)); + expect(cache['a'], equals(5)); + expect(calls, equals(2)); + }); + + test('getIfCached returns current value', () { + var cache = {'a': 1}; + expect(cache.getIfCached('a'), equals(1)); + expect(cache.getIfCached('b'), isNull); + }); + + test('getCachedAsync caches resolved value', () async { + var cache = {}; + var calls = 0; + + var v1 = await cache.getCachedAsync('a', () async { + calls++; + return 7; + }); + expect(v1, equals(7)); + + var v2 = await cache.getCachedAsync('a', () async { + calls++; + return 99; + }); + expect(v2, equals(7)); + expect(calls, equals(1)); + }); + + test('getCachedAsyncNullable does not cache null', () async { + var cache = {}; + + var v = await cache.getCachedAsyncNullable('a', () async => null); + expect(v, isNull); + expect(cache.containsKey('a'), isFalse); + + var v2 = await cache.getCachedAsyncNullable('a', () async => 3); + expect(v2, equals(3)); + expect(cache['a'], equals(3)); + }); + + test('checkCacheLimit evicts oldest entries', () { + var cache = {}; + for (var i = 0; i < 5; ++i) { + cache['k$i'] = i; + } + + expect(cache.checkCacheLimit(null), equals(0)); + expect(cache.length, equals(5)); + + var deleted = cache.checkCacheLimit(3); + expect(deleted, equals(2)); + expect(cache.length, equals(3)); + // first-inserted keys evicted + expect(cache.containsKey('k0'), isFalse); + expect(cache.containsKey('k1'), isFalse); + expect(cache.containsKey('k4'), isTrue); + + var cleared = cache.checkCacheLimit(0); + expect(cleared, equals(3)); + expect(cache.isEmpty, isTrue); + }); + + test('getCached enforces cacheLimit', () { + var cache = {}; + // Eviction runs before insertion, so length stays bounded near the limit. + for (var i = 0; i < 10; ++i) { + cache.getCached('k$i', () => i, cacheLimit: 2); + } + expect(cache.length, lessThanOrEqualTo(3)); + expect(cache.length, greaterThanOrEqualTo(2)); + }); + }); + + group('MapOfCachesExtension', () { + test('getMultiCached reuses equivalent (wildcard) cache', () { + var caches = <(String, bool), Map>{}; + + // Populate a wildcard cache (second field == true matches any query). + caches.populateMultiCache('k1', ('a', true), () => {}, 100); + + var computerCalled = false; + var v = caches.getMultiCached( + 'k1', + ('a', false), + () => {}, + () { + computerCalled = true; + return 999; + }, + ); + + expect(v, equals(100)); + expect(computerCalled, isFalse); + }); + + test('getMultiCached computes and caches on miss', () { + var caches = <(String, bool), Map>{}; + + var v = caches.getMultiCached( + 'k', + ('a', false), + () => {}, + () => 42, + ); + expect(v, equals(42)); + + // Now cached in its own context. + var v2 = caches.getMultiCached( + 'k', + ('a', false), + () => {}, + () => 0, + ); + expect(v2, equals(42)); + }); + + test('getMultiCachedNullable does not cache null', () { + var caches = <(String, bool), Map>{}; + + var v = caches.getMultiCachedNullable( + 'k', + ('a', false), + () => {}, + () => null, + ); + expect(v, isNull); + + var v2 = caches.getMultiCachedNullable( + 'k', + ('a', false), + () => {}, + () => 5, + ); + expect(v2, equals(5)); + }); + + test('getMultiCachedAsync resolves and caches', () async { + var caches = <(String, bool), Map>{}; + + var v = await caches.getMultiCachedAsync( + 'k', + ('a', false), + () => {}, + () async => 11, + ); + expect(v, equals(11)); + + var v2 = await caches.getMultiCachedAsync( + 'k', + ('a', false), + () => {}, + () async => 0, + ); + expect(v2, equals(11)); + }); + + test('isEquivalentContext wildcard semantics', () { + var caches = <(String, bool), Map>{}; + expect(caches.isEquivalentContext(('a', false), ('a', false)), isTrue); + expect(caches.isEquivalentContext(('a', true), ('a', false)), isTrue); + expect(caches.isEquivalentContext(('a', false), ('a', true)), isFalse); + expect(caches.isEquivalentContext(('a', false), ('b', false)), isFalse); + }); + + test('equivalentCaches excludes exact-context match', () { + var caches = <(String, bool), Map>{}; + caches.populateMultiCache('k', ('a', true), () => {}, 1); + caches.populateMultiCache('k', ('a', false), () => {}, 2); + + var equivalents = caches.equivalentCaches(('a', false)).toList(); + // Only the wildcard ('a', true) is equivalent; ('a', false) itself excluded. + expect(equivalents.length, equals(1)); + expect(equivalents.first['k'], equals(1)); + }); + }); + + group('RecordExtension', () { + test('positionalParametersLength', () { + expect((1,).positionalParametersLength, equals(1)); + expect((1, 2).positionalParametersLength, equals(2)); + expect((1, 2, 3).positionalParametersLength, equals(3)); + expect((1, 2, 3, 4).positionalParametersLength, equals(4)); + expect((1, 2, 3, 4, 5).positionalParametersLength, equals(5)); + // Cached on second access. + expect((9, 8).positionalParametersLength, equals(2)); + }); + }); }