diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 9f8494bd..de6bf129 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -869,6 +869,38 @@ jobs: needs: - job_001 job_028: + name: "all; PKG: packages/neon/neon; `flutter test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/neon/neon;commands:test_0" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/neon/neon + os:ubuntu-latest;pub-cache-hosted;sdk:stable + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Flutter SDK + uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + with: + channel: stable + - id: checkout + name: Checkout repository + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + - id: packages_neon_neon_pub_upgrade + name: packages/neon/neon; flutter pub upgrade + run: flutter pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: packages/neon/neon + - name: packages/neon/neon; flutter test + run: flutter test + if: "always() && steps.packages_neon_neon_pub_upgrade.conclusion == 'success'" + working-directory: packages/neon/neon + needs: + - job_001 + job_029: name: "all; PKG: packages/nextcloud; `dart test`" runs-on: ubuntu-latest steps: @@ -876,7 +908,7 @@ jobs: uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/nextcloud;commands:test" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/nextcloud;commands:test_1" restore-keys: | os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/nextcloud os:ubuntu-latest;pub-cache-hosted;sdk:stable @@ -900,7 +932,7 @@ jobs: working-directory: packages/nextcloud needs: - job_001 - job_029: + job_030: name: "all; PKG: packages/sort_box; `dart test`" runs-on: ubuntu-latest steps: @@ -908,7 +940,7 @@ jobs: uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/sort_box;commands:test" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/sort_box;commands:test_1" restore-keys: | os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:packages/sort_box os:ubuntu-latest;pub-cache-hosted;sdk:stable diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index b30ad6bb..2bc8e73f 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -56,6 +56,7 @@ part 'src/blocs/first_launch.dart'; part 'src/blocs/login.dart'; part 'src/blocs/next_push.dart'; part 'src/blocs/push_notifications.dart'; +part 'src/blocs/timer.dart'; part 'src/blocs/user_details.dart'; part 'src/blocs/user_status.dart'; part 'src/interfaces/notifications.dart'; diff --git a/packages/neon/neon/lib/src/blocs/timer.dart b/packages/neon/neon/lib/src/blocs/timer.dart new file mode 100644 index 00000000..c19f327c --- /dev/null +++ b/packages/neon/neon/lib/src/blocs/timer.dart @@ -0,0 +1,85 @@ +part of '../../neon.dart'; + +abstract class TimerBlocEvents { + /// Register a [callback] that will be called periodically. + /// The time between the executions is defined by the [duration]. + NeonTimer registerTimer(final Duration duration, final VoidCallback callback); + + /// Unregister a timer that has been previously registered with the bloc. + /// You can also use [NeonTimer.cancel]. + void unregisterTimer(final NeonTimer timer); +} + +abstract class TimerBlocStates {} + +/// Execute callbacks at defined periodic intervals. +/// Components can register their callbacks and everything with the same periodicity will be executed at the same time. +/// +/// The [TimerBloc] is a singleton. +/// Sub-second timers are not supported. +class TimerBloc extends Bloc implements TimerBlocEvents, TimerBlocStates { + factory TimerBloc() => instance ??= TimerBloc._(); + + @visibleForTesting + factory TimerBloc.mocked(final TimerBloc mock) => instance ??= mock; + + TimerBloc._(); + + @visibleForTesting + static TimerBloc? instance; + + final Map _timers = {}; + final Map> _callbacks = {}; + + @visibleForTesting + Map get timers => _timers; + + @visibleForTesting + Map> get callbacks => _callbacks; + + @override + void dispose() { + for (final timer in _timers.values) { + timer.cancel(); + } + _timers.clear(); + _callbacks.clear(); + TimerBloc.instance = null; + } + + @override + NeonTimer registerTimer(final Duration duration, final VoidCallback callback) { + if (_timers[duration.inSeconds] == null) { + _timers[duration.inSeconds] = Timer.periodic(duration, (final _) { + for (final callback in _callbacks[duration.inSeconds]!) { + callback(); + } + }); + _callbacks[duration.inSeconds] = {callback}; + } else { + _callbacks[duration.inSeconds]!.add(callback); + } + return NeonTimer(duration, callback); + } + + @override + void unregisterTimer(final NeonTimer timer) { + if (_timers[timer.duration.inSeconds] != null) { + _callbacks[timer.duration.inSeconds]!.remove(timer.callback); + } + } +} + +class NeonTimer { + NeonTimer( + this.duration, + this.callback, + ); + + final Duration duration; + final VoidCallback callback; + + void cancel() { + TimerBloc().unregisterTimer(this); + } +} diff --git a/packages/neon/neon/mono_pkg.yaml b/packages/neon/neon/mono_pkg.yaml index 60bc3bfd..a6c495b0 100644 --- a/packages/neon/neon/mono_pkg.yaml +++ b/packages/neon/neon/mono_pkg.yaml @@ -5,3 +5,4 @@ stages: - all: - analyze: --fatal-infos . - format: --output=none --set-exit-if-changed --line-length 120 . + - test diff --git a/packages/neon/neon/pubspec.yaml b/packages/neon/neon/pubspec.yaml index 5bca6d64..3cd0244e 100644 --- a/packages/neon/neon/pubspec.yaml +++ b/packages/neon/neon/pubspec.yaml @@ -64,6 +64,7 @@ dev_dependencies: git: url: https://github.com/stack11/dart_nit_picking ref: 0b2ee0d + test: ^1.24.3 dependency_overrides: wakelock_windows: # TODO: https://github.com/creativecreatorormaybenot/wakelock/pull/195 diff --git a/packages/neon/neon/test/timer_bloc_test.dart b/packages/neon/neon/test/timer_bloc_test.dart new file mode 100644 index 00000000..504e23df --- /dev/null +++ b/packages/neon/neon/test/timer_bloc_test.dart @@ -0,0 +1,43 @@ +import 'package:neon/neon.dart'; +import 'package:test/test.dart'; + +void main() { + group('TimerBloc', () { + tearDown(() { + TimerBloc().dispose(); + }); + + test('Register timer', () async { + const duration = Duration(milliseconds: 100); + + final stopwatch = Stopwatch()..start(); + final callback = stopwatch.stop; + TimerBloc().registerTimer(duration, callback); + await Future.delayed(duration); + + expect(stopwatch.elapsedMilliseconds, greaterThan(duration.inMilliseconds)); + expect(stopwatch.elapsedMilliseconds, lessThan(duration.inMilliseconds * 1.1)); + expect(TimerBloc().callbacks[duration.inSeconds], contains(callback)); + expect(TimerBloc().timers[duration.inSeconds], isNot(isNull)); + }); + + test('Unregister timer', () async { + const duration = Duration(milliseconds: 100); + final callback = neverCalled; + + TimerBloc().registerTimer(duration, callback).cancel(); + await Future.delayed(duration); + + expect(TimerBloc().callbacks[duration.inSeconds], isNot(contains(callback))); + }); + + test('dispose', () { + TimerBloc().registerTimer(const Duration(minutes: 1), () {}); + expect(TimerBloc().timers, hasLength(1)); + expect(TimerBloc().callbacks, hasLength(1)); + TimerBloc().dispose(); + expect(TimerBloc().timers, isEmpty); + expect(TimerBloc().callbacks, isEmpty); + }); + }); +} diff --git a/tool/ci.sh b/tool/ci.sh index 59aa55ff..b07eaafb 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -79,7 +79,11 @@ for PKG in ${PKGS}; do echo 'dart format --output=none --set-exit-if-changed --line-length 120 .' dart format --output=none --set-exit-if-changed --line-length 120 . || EXIT_CODE=$? ;; - test) + test_0) + echo 'flutter test' + flutter test || EXIT_CODE=$? + ;; + test_1) echo 'dart test' dart test || EXIT_CODE=$? ;;