diff --git a/.github/ISSUE_TEMPLATE/args.md b/.github/ISSUE_TEMPLATE/args.md new file mode 100644 index 00000000..0a99d177 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/args.md @@ -0,0 +1,5 @@ +--- +name: "package:args" +about: "Create a bug or file a feature request against package:args." +labels: "package:args" +--- diff --git a/.github/ISSUE_TEMPLATE/logging.md b/.github/ISSUE_TEMPLATE/logging.md new file mode 100644 index 00000000..53564bc7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/logging.md @@ -0,0 +1,5 @@ +--- +name: "package:logging" +about: "Create a bug or file a feature request against package:logging." +labels: "package:logging" +--- \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/os_detect.md b/.github/ISSUE_TEMPLATE/os_detect.md new file mode 100644 index 00000000..4648d154 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/os_detect.md @@ -0,0 +1,5 @@ +--- +name: "package:os_detect" +about: "Create a bug or file a feature request against package:os_detect." +labels: "package:os_detect" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index d74baa1e..03fe1f47 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -24,6 +24,14 @@ - changed-files: - any-glob-to-any-file: 'pkgs/fixnum/**' +"package:logging": + - changed-files: + - any-glob-to-any-file: 'pkgs/logging/**' + +"package:os_detect": + - changed-files: + - any-glob-to-any-file: 'pkgs/os_detect/**' + "package:path": - changed-files: - any-glob-to-any-file: 'pkgs/path/**' diff --git a/.github/workflows/logging.yaml b/.github/workflows/logging.yaml new file mode 100644 index 00000000..c8666412 --- /dev/null +++ b/.github/workflows/logging.yaml @@ -0,0 +1,70 @@ +name: package:logging + +on: + # Run CI on pushes to the main branch, and on PRs against main. + push: + branches: [ main ] + paths: + - '.github/workflows/logging.yaml' + - 'pkgs/logging/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/logging.yaml' + - 'pkgs/logging/**' + schedule: + - cron: "0 0 * * 0" +env: + PUB_ENVIRONMENT: bot.github + +defaults: + run: + working-directory: pkgs/logging/ + +jobs: + # Check code formatting and static analysis. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix of platforms and sdk versions. + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: dart test --platform chrome + if: always() && steps.install.outcome == 'success' diff --git a/.github/workflows/os_detect.yaml b/.github/workflows/os_detect.yaml new file mode 100644 index 00000000..be86b67e --- /dev/null +++ b/.github/workflows/os_detect.yaml @@ -0,0 +1,73 @@ +name: package:os_detect + +on: + # Run CI on pushes to the main branch, and on PRs against main. + push: + branches: [ main ] + paths: + - '.github/workflows/os_detect.yaml' + - 'pkgs/os_detect/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/os_detect.yaml' + - 'pkgs/os_detect/**' + schedule: + - cron: "0 0 * * 0" +env: + PUB_ENVIRONMENT: bot.github + +defaults: + run: + working-directory: pkgs/os_detect/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest, windows-latest, macos-latest] + sdk: [3.0.0, dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: dart test --platform chrome + if: always() && steps.install.outcome == 'success' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4646827..5b4fcef5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ new file please also add this. The year should be a single number stating the year the file was created (don't use a range like "2011-2012"). Additionally, if you edit an existing file, you shouldn't update the year. - // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file + // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. @@ -48,4 +48,4 @@ This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). We pledge to maintain an open and welcoming environment. For details, see our -[code of conduct](https://dart.dev/code-of-conduct). \ No newline at end of file +[code of conduct](https://dart.dev/code-of-conduct). diff --git a/README.md b/README.md index dfa73dae..4565c818 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ This repository is home to various Dart packages under the [dart.dev](https://pu | [convert](pkgs/convert/) | Utilities for converting between data representations. | [![pub package](https://img.shields.io/pub/v/convert.svg)](https://pub.dev/packages/convert) | | [crypto](pkgs/crypto/) | Implementations of SHA, MD5, and HMAC cryptographic functions. | [![pub package](https://img.shields.io/pub/v/crypto.svg)](https://pub.dev/packages/crypto) | | [fixnum](pkgs/fixnum/) | Library for 32- and 64-bit signed fixed-width integers. | [![pub package](https://img.shields.io/pub/v/fixnum.svg)](https://pub.dev/packages/fixnum) | +| [logging](pkgs/logging/) | Provides APIs for debugging and error logging. | [![pub package](https://img.shields.io/pub/v/logging.svg)](https://pub.dev/packages/logging) | +| [os_detect](pkgs/os_detect/) | Platform independent OS detection. | [![pub package](https://img.shields.io/pub/v/os_detect.svg)](https://pub.dev/packages/os_detect) | | [path](pkgs/path/) | A string-based path manipulation library for all of the path operations you know and love. | [![pub package](https://img.shields.io/pub/v/path.svg)](https://pub.dev/packages/path) | ## Publishing automation diff --git a/pkgs/args/example/arg_parser/pubspec.yaml b/pkgs/args/example/arg_parser/pubspec.yaml index 52f193a8..9cb1cbdc 100644 --- a/pkgs/args/example/arg_parser/pubspec.yaml +++ b/pkgs/args/example/arg_parser/pubspec.yaml @@ -6,8 +6,10 @@ name: arg_parser_example version: 1.0.0 description: An example of using ArgParser publish_to: 'none' + environment: - sdk: '>=2.14.0 <3.0.0' + sdk: ^3.0.0 + dependencies: args: path: ../.. diff --git a/pkgs/args/example/command_runner/pubspec.yaml b/pkgs/args/example/command_runner/pubspec.yaml index 0745be61..636b4641 100644 --- a/pkgs/args/example/command_runner/pubspec.yaml +++ b/pkgs/args/example/command_runner/pubspec.yaml @@ -6,8 +6,10 @@ name: command_runner_example version: 1.0.0 description: An example of using CommandRunner publish_to: 'none' + environment: - sdk: '>=2.14.0 <3.0.0' + sdk: ^3.0.0 + dependencies: args: path: ../.. diff --git a/pkgs/logging/.gitignore b/pkgs/logging/.gitignore new file mode 100644 index 00000000..79f51c3d --- /dev/null +++ b/pkgs/logging/.gitignore @@ -0,0 +1,3 @@ +.dart_tool +.packages +pubspec.lock diff --git a/pkgs/logging/AUTHORS b/pkgs/logging/AUTHORS new file mode 100644 index 00000000..630ab0e7 --- /dev/null +++ b/pkgs/logging/AUTHORS @@ -0,0 +1,10 @@ +# Names should be added to this file with this pattern: +# +# For individuals: +# Name +# +# For organizations: +# Organization +# +Google Inc. <*@google.com> +Anton Astashov diff --git a/pkgs/logging/CHANGELOG.md b/pkgs/logging/CHANGELOG.md new file mode 100644 index 00000000..088183c9 --- /dev/null +++ b/pkgs/logging/CHANGELOG.md @@ -0,0 +1,92 @@ +## 1.3.0 + +* Override empty stack traces for trace level events. +* Require Dart 3.4 +* Move to `dart-lang/core` monorepo. + +## 1.2.0 + +* Add notification when the log level is changed. Logger `onLevelChanged` broadcasts a stream of level values. +* Require Dart 2.19. + +## 1.1.1 + +* Add a check that throws if a logger name ends with '.'. +* Require Dart 2.18 + +## 1.1.0 + +* Add `Logger.attachedLoggers` which exposes all loggers created with the + default constructor. +* Enable the `avoid_dynamic_calls` lint. + +## 1.0.2 + +* Update description. +* Add example. + +## 1.0.1 + +* List log levels in README. + +## 1.0.0 + +* Stable null safety release. + +## 1.0.0-nullsafety.0 + +* Migrate to null safety. +* Removed the deprecated `LoggerHandler` typedef. + +## 0.11.4 + +* Add top level `defaultLevel`. +* Require Dart `>=2.0.0`. +* Make detached loggers work regardless of `hierarchicalLoggingEnabled`. + +## 0.11.3+2 + +* Set max SDK version to `<3.0.0`, and adjust other dependencies. + +## 0.11.3+1 + +* Fixed several documentation comments. + +## 0.11.3 + +* Added optional `LogRecord.object` field. + +* `Logger.log` sets `LogRecord.object` if the message is not a string or a + function that returns a string. So that a handler can access the original + object instead of just its `toString()`. + +## 0.11.2 + +* Added `Logger.detached` - a convenience factory to obtain a logger that is not + attached to this library's logger hierarchy. + +## 0.11.1+1 + +* Include default error with the auto-generated stack traces. + +## 0.11.1 + +* Add support for automatically logging the stack trace on error messages. Note + this can be expensive, so it is off by default. + +## 0.11.0 + +* Revert change in `0.10.0`. `stackTrace` must be an instance of `StackTrace`. + Use the `Trace` class from the [stack_trace package][] to convert strings. + +[stack_trace package]: https://pub.dev/packages/stack_trace + +## 0.10.0 + +* Change type of `stackTrace` from `StackTrace` to `Object`. + +## 0.9.3 + +* Added optional `LogRecord.zone` field. + +* Record current zone (or user specified zone) when creating new `LogRecord`s. diff --git a/pkgs/logging/LICENSE b/pkgs/logging/LICENSE new file mode 100644 index 00000000..ab3bfa01 --- /dev/null +++ b/pkgs/logging/LICENSE @@ -0,0 +1,27 @@ +Copyright 2013, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/logging/README.md b/pkgs/logging/README.md new file mode 100644 index 00000000..66231450 --- /dev/null +++ b/pkgs/logging/README.md @@ -0,0 +1,141 @@ +[![Dart CI](https://github.com/dart-lang/core/actions/workflows/logging.yaml/badge.svg)](https://github.com/dart-lang/core/actions/workflows/logging.yaml) +[![Pub](https://img.shields.io/pub/v/logging.svg)](https://pub.dev/packages/logging) +[![package publisher](https://img.shields.io/pub/publisher/logging.svg)](https://pub.dev/packages/logging/publisher) + +## Initializing + +By default, the logging package does not do anything useful with the log +messages. You must configure the logging level and add a handler for the log +messages. + +Here is a simple logging configuration that logs all messages via `print`. + +```dart +Logger.root.level = Level.ALL; // defaults to Level.INFO +Logger.root.onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); +}); +``` + +First, set the root `Level`. All messages at or above the current level are sent to the +`onRecord` stream. Available levels are: + ++ `Level.OFF` ++ `Level.SHOUT` ++ `Level.SEVERE` ++ `Level.WARNING` ++ `Level.INFO` ++ `Level.CONFIG` ++ `Level.FINE` ++ `Level.FINER` ++ `Level.FINEST` + +Then, listen on the `onRecord` stream for `LogRecord` events. The `LogRecord` +class has various properties for the message, error, logger name, and more. + +To listen for changed level notifications use: + +```dart +Logger.root.onLevelChanged.listen((level) { + print('The new log level is $level'); +}); +``` + +## Logging messages + +Create a `Logger` with a unique name to easily identify the source of the log +messages. + +```dart +final log = Logger('MyClassName'); +``` + +Here is an example of logging a debug message and an error: + +```dart +var future = doSomethingAsync().then((result) { + log.fine('Got the result: $result'); + processResult(result); +}).catchError((e, stackTrace) => log.severe('Oh noes!', e, stackTrace)); +``` + +When logging more complex messages, you can pass a closure instead that will be +evaluated only if the message is actually logged: + +```dart +log.fine(() => [1, 2, 3, 4, 5].map((e) => e * 4).join("-")); +``` + +Available logging methods are: + ++ `log.shout(logged_content);` ++ `log.severe(logged_content);` ++ `log.warning(logged_content);` ++ `log.info(logged_content);` ++ `log.config(logged_content);` ++ `log.fine(logged_content);` ++ `log.finer(logged_content);` ++ `log.finest(logged_content);` + +## Configuration + +Loggers can be individually configured and listened to. When an individual logger has no +specific configuration, it uses the configuration and any listeners found at `Logger.root`. + +To begin, set the global boolean `hierarchicalLoggingEnabled` to `true`. + +Then, create unique loggers and configure their `level` attributes and assign any listeners to +their `onRecord` streams. + + +```dart + hierarchicalLoggingEnabled = true; + Logger.root.level = Level.WARNING; + Logger.root.onRecord.listen((record) { + print('[ROOT][WARNING+] ${record.message}'); + }); + + final log1 = Logger('FINE+'); + log1.level = Level.FINE; + log1.onRecord.listen((record) { + print('[LOG1][FINE+] ${record.message}'); + }); + + // log2 inherits LEVEL value of WARNING from `Logger.root` + final log2 = Logger('WARNING+'); + log2.onRecord.listen((record) { + print('[LOG2][WARNING+] ${record.message}'); + }); + + + // Will NOT print because FINER is too low level for `Logger.root`. + log1.finer('LOG_01 FINER (X)'); + + // Will print twice ([LOG1] & [ROOT]) + log1.fine('LOG_01 FINE (√√)'); + + // Will print ONCE because `log1` only uses root listener. + log1.warning('LOG_01 WARNING (√)'); + + // Will never print because FINE is too low level. + log2.fine('LOG_02 FINE (X)'); + + // Will print twice ([LOG2] & [ROOT]) because warning is sufficient for all + // loggers' levels. + log2.warning('LOG_02 WARNING (√√)'); + + // Will never print because `info` is filtered by `Logger.root.level` of + // `Level.WARNING`. + log2.info('INFO (X)'); +``` + +Results in: + +``` +[LOG1][FINE+] LOG_01 FINE (√√) +[ROOT][WARNING+] LOG_01 FINE (√√) +[LOG1][FINE+] LOG_01 WARNING (√) +[ROOT][WARNING+] LOG_01 WARNING (√) +[LOG2][WARNING+] LOG_02 WARNING (√√) +[ROOT][WARNING+] LOG_02 WARNING (√√) +``` diff --git a/pkgs/logging/analysis_options.yaml b/pkgs/logging/analysis_options.yaml new file mode 100644 index 00000000..7c004e0f --- /dev/null +++ b/pkgs/logging/analysis_options.yaml @@ -0,0 +1,31 @@ +# https://dart.dev/guides/language/analysis-options +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-raw-types: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_unused_constructor_parameters + - avoid_void_async + - cancel_subscriptions + - join_return_with_assignment + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - package_api_docs + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - unnecessary_await_in_return + - unnecessary_raw_strings + - use_if_null_to_convert_nulls_to_bools + - use_raw_strings + - use_string_buffers diff --git a/pkgs/logging/example/main.dart b/pkgs/logging/example/main.dart new file mode 100644 index 00000000..b565c3c1 --- /dev/null +++ b/pkgs/logging/example/main.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:logging/logging.dart'; + +final log = Logger('ExampleLogger'); + +/// Example of configuring a logger to print to stdout. +/// +/// This example will print: +/// +/// INFO: 2021-09-13 15:35:10.703401: recursion: n = 4 +/// INFO: 2021-09-13 15:35:10.707974: recursion: n = 3 +/// Fibonacci(4) is: 3 +/// Fibonacci(5) is: 5 +/// SHOUT: 2021-09-13 15:35:10.708087: Unexpected negative n: -42 +/// Fibonacci(-42) is: 1 +void main() { + Logger.root.level = Level.ALL; // defaults to Level.INFO + Logger.root.onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }); + + print('Fibonacci(4) is: ${fibonacci(4)}'); + + Logger.root.level = Level.SEVERE; // skip logs less then severe. + print('Fibonacci(5) is: ${fibonacci(5)}'); + + print('Fibonacci(-42) is: ${fibonacci(-42)}'); +} + +int fibonacci(int n) { + if (n <= 2) { + if (n < 0) log.shout('Unexpected negative n: $n'); + return 1; + } else { + log.info('recursion: n = $n'); + return fibonacci(n - 2) + fibonacci(n - 1); + } +} diff --git a/pkgs/logging/lib/logging.dart b/pkgs/logging/lib/logging.dart new file mode 100644 index 00000000..6f447e7c --- /dev/null +++ b/pkgs/logging/lib/logging.dart @@ -0,0 +1,7 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'src/level.dart'; +export 'src/log_record.dart'; +export 'src/logger.dart'; diff --git a/pkgs/logging/lib/src/level.dart b/pkgs/logging/lib/src/level.dart new file mode 100644 index 00000000..36694335 --- /dev/null +++ b/pkgs/logging/lib/src/level.dart @@ -0,0 +1,88 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: constant_identifier_names + +/// [Level]s to control logging output. Logging can be enabled to include all +/// levels above certain [Level]. [Level]s are ordered using an integer +/// value [Level.value]. The predefined [Level] constants below are sorted as +/// follows (in descending order): [Level.SHOUT], [Level.SEVERE], +/// [Level.WARNING], [Level.INFO], [Level.CONFIG], [Level.FINE], [Level.FINER], +/// [Level.FINEST], and [Level.ALL]. +/// +/// We recommend using one of the predefined logging levels. If you define your +/// own level, make sure you use a value between those used in [Level.ALL] and +/// [Level.OFF]. +class Level implements Comparable { + final String name; + + /// Unique value for this level. Used to order levels, so filtering can + /// exclude messages whose level is under certain value. + final int value; + + const Level(this.name, this.value); + + /// Special key to turn on logging for all levels ([value] = 0). + static const Level ALL = Level('ALL', 0); + + /// Special key to turn off all logging ([value] = 2000). + static const Level OFF = Level('OFF', 2000); + + /// Key for highly detailed tracing ([value] = 300). + static const Level FINEST = Level('FINEST', 300); + + /// Key for fairly detailed tracing ([value] = 400). + static const Level FINER = Level('FINER', 400); + + /// Key for tracing information ([value] = 500). + static const Level FINE = Level('FINE', 500); + + /// Key for static configuration messages ([value] = 700). + static const Level CONFIG = Level('CONFIG', 700); + + /// Key for informational messages ([value] = 800). + static const Level INFO = Level('INFO', 800); + + /// Key for potential problems ([value] = 900). + static const Level WARNING = Level('WARNING', 900); + + /// Key for serious failures ([value] = 1000). + static const Level SEVERE = Level('SEVERE', 1000); + + /// Key for extra debugging loudness ([value] = 1200). + static const Level SHOUT = Level('SHOUT', 1200); + + static const List LEVELS = [ + ALL, + FINEST, + FINER, + FINE, + CONFIG, + INFO, + WARNING, + SEVERE, + SHOUT, + OFF + ]; + + @override + bool operator ==(Object other) => other is Level && value == other.value; + + bool operator <(Level other) => value < other.value; + + bool operator <=(Level other) => value <= other.value; + + bool operator >(Level other) => value > other.value; + + bool operator >=(Level other) => value >= other.value; + + @override + int compareTo(Level other) => value - other.value; + + @override + int get hashCode => value; + + @override + String toString() => name; +} diff --git a/pkgs/logging/lib/src/log_record.dart b/pkgs/logging/lib/src/log_record.dart new file mode 100644 index 00000000..8a0ee618 --- /dev/null +++ b/pkgs/logging/lib/src/log_record.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'level.dart'; +import 'logger.dart'; + +/// A log entry representation used to propagate information from [Logger] to +/// individual handlers. +class LogRecord { + final Level level; + final String message; + + /// Non-string message passed to Logger. + final Object? object; + + /// Logger where this record is stored. + final String loggerName; + + /// Time when this record was created. + final DateTime time; + + /// Unique sequence number greater than all log records created before it. + final int sequenceNumber; + + static int _nextNumber = 0; + + /// Associated error (if any) when recording errors messages. + final Object? error; + + /// Associated stackTrace (if any) when recording errors messages. + final StackTrace? stackTrace; + + /// Zone of the calling code which resulted in this LogRecord. + final Zone? zone; + + LogRecord(this.level, this.message, this.loggerName, + [this.error, this.stackTrace, this.zone, this.object]) + : time = DateTime.now(), + sequenceNumber = LogRecord._nextNumber++; + + @override + String toString() => '[${level.name}] $loggerName: $message'; +} diff --git a/pkgs/logging/lib/src/logger.dart b/pkgs/logging/lib/src/logger.dart new file mode 100644 index 00000000..d9fa2544 --- /dev/null +++ b/pkgs/logging/lib/src/logger.dart @@ -0,0 +1,326 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; + +import 'level.dart'; +import 'log_record.dart'; + +/// Whether to allow fine-grain logging and configuration of loggers in a +/// hierarchy. +/// +/// When false, all hierarchical logging instead is merged in the root logger. +bool hierarchicalLoggingEnabled = false; + +/// Automatically record stack traces for any message of this level or above. +/// +/// Because this is expensive, this is off by default. +Level recordStackTraceAtLevel = Level.OFF; + +/// The default [Level]. +const defaultLevel = Level.INFO; + +/// Use a [Logger] to log debug messages. +/// +/// [Logger]s are named using a hierarchical dot-separated name convention. +class Logger { + /// Simple name of this logger. + final String name; + + /// The full name of this logger, which includes the parent's full name. + String get fullName => + parent?.name.isNotEmpty ?? false ? '${parent!.fullName}.$name' : name; + + /// Parent of this logger in the hierarchy of loggers. + final Logger? parent; + + /// Logging [Level] used for entries generated on this logger. + /// + /// Only the root logger is guaranteed to have a non-null [Level]. + Level? _level; + + /// Private modifiable map of child loggers, indexed by their simple names. + final Map _children; + + /// Children in the hierarchy of loggers, indexed by their simple names. + /// + /// This is an unmodifiable map. + final Map children; + + /// Controller used to notify when log entries are added to this logger. + /// + /// If hierarchical logging is disabled then this is `null` for all but the + /// root [Logger]. + StreamController? _controller; + + /// Controller used to notify when the log level of this logger is changed. + StreamController? _levelChangedController; + + /// Create or find a Logger by name. + /// + /// Calling `Logger(name)` will return the same instance whenever it is called + /// with the same string name. Loggers created with this constructor are + /// retained indefinitely and available through [attachedLoggers]. + factory Logger(String name) => + _loggers.putIfAbsent(name, () => Logger._named(name)); + + /// Creates a new detached [Logger]. + /// + /// Returns a new [Logger] instance (unlike `new Logger`, which returns a + /// [Logger] singleton), which doesn't have any parent or children, + /// and is not a part of the global hierarchical loggers structure. + /// + /// It can be useful when you just need a local short-living logger, + /// which you'd like to be garbage-collected later. + factory Logger.detached(String name) => + Logger._internal(name, null, {}); + + factory Logger._named(String name) { + if (name.startsWith('.')) { + throw ArgumentError("name shouldn't start with a '.'"); + } + if (name.endsWith('.')) { + throw ArgumentError("name shouldn't end with a '.'"); + } + + // Split hierarchical names (separated with '.'). + final dot = name.lastIndexOf('.'); + Logger? parent; + String thisName; + if (dot == -1) { + if (name != '') parent = Logger(''); + thisName = name; + } else { + parent = Logger(name.substring(0, dot)); + thisName = name.substring(dot + 1); + } + return Logger._internal(thisName, parent, {}); + } + + Logger._internal(this.name, this.parent, Map children) + : _children = children, + children = UnmodifiableMapView(children) { + if (parent == null) { + _level = defaultLevel; + } else { + parent!._children[name] = this; + } + } + + /// Effective level considering the levels established in this logger's + /// parents (when [hierarchicalLoggingEnabled] is true). + Level get level { + Level effectiveLevel; + + if (parent == null) { + // We're either the root logger or a detached logger. Return our own + // level. + effectiveLevel = _level!; + } else if (!hierarchicalLoggingEnabled) { + effectiveLevel = root._level!; + } else { + effectiveLevel = _level ?? parent!.level; + } + + // ignore: unnecessary_null_comparison + assert(effectiveLevel != null); + return effectiveLevel; + } + + /// Override the level for this particular [Logger] and its children. + /// + /// Setting this to `null` makes it inherit the [parent]s level. + set level(Level? value) { + if (!hierarchicalLoggingEnabled && parent != null) { + throw UnsupportedError( + 'Please set "hierarchicalLoggingEnabled" to true if you want to ' + 'change the level on a non-root logger.'); + } + if (parent == null && value == null) { + throw UnsupportedError( + 'Cannot set the level to `null` on a logger with no parent.'); + } + final isLevelChanged = _level != value; + _level = value; + if (isLevelChanged) { + _levelChangedController?.add(value); + } + } + + /// Returns a stream of level values set to this [Logger]. + /// + /// You can listen for set levels using the standard stream APIs, + /// for instance: + /// + /// ```dart + /// logger.onLevelChanged.listen((level) { ... }); + /// ``` + /// A state error will be thrown if the level is changed + /// inside the callback. + Stream get onLevelChanged { + _levelChangedController ??= StreamController.broadcast(sync: true); + return _levelChangedController!.stream; + } + + /// Returns a stream of messages added to this [Logger]. + /// + /// You can listen for messages using the standard stream APIs, for instance: + /// + /// ```dart + /// logger.onRecord.listen((record) { ... }); + /// ``` + Stream get onRecord => _getStream(); + + void clearListeners() { + if (hierarchicalLoggingEnabled || parent == null) { + _controller?.close(); + _controller = null; + } else { + root.clearListeners(); + } + } + + /// Whether a message for [value]'s level is loggable in this logger. + bool isLoggable(Level value) => value >= level; + + /// Adds a log record for a [message] at a particular [logLevel] if + /// `isLoggable(logLevel)` is true. + /// + /// Use this method to create log entries for user-defined levels. To record a + /// message at a predefined level (e.g. [Level.INFO], [Level.WARNING], etc) + /// you can use their specialized methods instead (e.g. [info], [warning], + /// etc). + /// + /// If [message] is a [Function], it will be lazy evaluated. Additionally, if + /// [message] or its evaluated value is not a [String], then 'toString()' will + /// be called on the object and the result will be logged. The log record will + /// contain a field holding the original object. + /// + /// The log record will also contain a field for the zone in which this call + /// was made. This can be advantageous if a log listener wants to handler + /// records of different zones differently (e.g. group log records by HTTP + /// request if each HTTP request handler runs in it's own zone). + /// + /// If this record is logged at a level equal to or higher than + /// [recordStackTraceAtLevel] and [stackTrace] is `null` or [StackTrace.empty] + /// it will be defaulted to the current stack trace for this call. + void log(Level logLevel, Object? message, + [Object? error, StackTrace? stackTrace, Zone? zone]) { + Object? object; + if (isLoggable(logLevel)) { + if (message is Function) { + message = (message as Object? Function())(); + } + + String msg; + if (message is String) { + msg = message; + } else { + msg = message.toString(); + object = message; + } + + if ((stackTrace == null || stackTrace == StackTrace.empty) && + logLevel >= recordStackTraceAtLevel) { + stackTrace = StackTrace.current; + error ??= 'autogenerated stack trace for $logLevel $msg'; + } + zone ??= Zone.current; + + final record = + LogRecord(logLevel, msg, fullName, error, stackTrace, zone, object); + + if (parent == null) { + _publish(record); + } else if (!hierarchicalLoggingEnabled) { + root._publish(record); + } else { + Logger? target = this; + while (target != null) { + target._publish(record); + target = target.parent; + } + } + } + } + + /// Log message at level [Level.FINEST]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void finest(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.FINEST, message, error, stackTrace); + + /// Log message at level [Level.FINER]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void finer(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.FINER, message, error, stackTrace); + + /// Log message at level [Level.FINE]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void fine(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.FINE, message, error, stackTrace); + + /// Log message at level [Level.CONFIG]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void config(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.CONFIG, message, error, stackTrace); + + /// Log message at level [Level.INFO]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void info(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.INFO, message, error, stackTrace); + + /// Log message at level [Level.WARNING]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void warning(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.WARNING, message, error, stackTrace); + + /// Log message at level [Level.SEVERE]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void severe(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.SEVERE, message, error, stackTrace); + + /// Log message at level [Level.SHOUT]. + /// + /// See [log] for information on how non-String [message] arguments are + /// handled. + void shout(Object? message, [Object? error, StackTrace? stackTrace]) => + log(Level.SHOUT, message, error, stackTrace); + + Stream _getStream() { + if (hierarchicalLoggingEnabled || parent == null) { + return (_controller ??= StreamController.broadcast(sync: true)) + .stream; + } else { + return root._getStream(); + } + } + + void _publish(LogRecord record) => _controller?.add(record); + + /// Top-level root [Logger]. + static final Logger root = Logger(''); + + /// All attached [Logger]s in the system. + static final Map _loggers = {}; + + /// All attached [Logger]s in the system. + /// + /// Loggers created with [Logger.detached] are not included. + static Iterable get attachedLoggers => _loggers.values; +} diff --git a/pkgs/logging/pubspec.yaml b/pkgs/logging/pubspec.yaml new file mode 100644 index 00000000..8fb8a436 --- /dev/null +++ b/pkgs/logging/pubspec.yaml @@ -0,0 +1,17 @@ +name: logging +version: 1.3.0 +description: >- + Provides APIs for debugging and error logging, similar to loggers in other + languages, such as the Closure JS Logger and java.util.logging.Logger. +repository: https://github.com/dart-lang/core/tree/main/pkgs/logging + +topics: + - logging + - debugging + +environment: + sdk: ^3.4.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.16.0 diff --git a/pkgs/logging/test/logging_test.dart b/pkgs/logging/test/logging_test.dart new file mode 100644 index 00000000..6bff3d86 --- /dev/null +++ b/pkgs/logging/test/logging_test.dart @@ -0,0 +1,761 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +void main() { + final hierarchicalLoggingEnabledDefault = hierarchicalLoggingEnabled; + + test('level comparison is a valid comparator', () { + const level1 = Level('NOT_REAL1', 253); + expect(level1 == level1, isTrue); + expect(level1 <= level1, isTrue); + expect(level1 >= level1, isTrue); + expect(level1 < level1, isFalse); + expect(level1 > level1, isFalse); + + const level2 = Level('NOT_REAL2', 455); + expect(level1 <= level2, isTrue); + expect(level1 < level2, isTrue); + expect(level2 >= level1, isTrue); + expect(level2 > level1, isTrue); + + const level3 = Level('NOT_REAL3', 253); + expect(level1, isNot(same(level3))); // different instances + expect(level1, equals(level3)); // same value. + }); + + test('default levels are in order', () { + const levels = Level.LEVELS; + + for (var i = 0; i < levels.length; i++) { + for (var j = i + 1; j < levels.length; j++) { + expect(levels[i] < levels[j], isTrue); + } + } + }); + + test('levels are comparable', () { + final unsorted = [ + Level.INFO, + Level.CONFIG, + Level.FINE, + Level.SHOUT, + Level.OFF, + Level.FINER, + Level.ALL, + Level.WARNING, + Level.FINEST, + Level.SEVERE, + ]; + + const sorted = Level.LEVELS; + + expect(unsorted, isNot(orderedEquals(sorted))); + + unsorted.sort(); + expect(unsorted, orderedEquals(sorted)); + }); + + test('levels are hashable', () { + final map = {}; + map[Level.INFO] = 'info'; + map[Level.SHOUT] = 'shout'; + expect(map[Level.INFO], same('info')); + expect(map[Level.SHOUT], same('shout')); + }); + + test('logger name cannot start with a "." ', () { + expect(() => Logger('.c'), throwsArgumentError); + }); + + test('logger name cannot end with a "."', () { + expect(() => Logger('a.'), throwsArgumentError); + expect(() => Logger('a..d'), throwsArgumentError); + }); + + test('root level has proper defaults', () { + expect(Logger.root, isNotNull); + expect(Logger.root.parent, null); + expect(Logger.root.level, defaultLevel); + }); + + test('logger naming is hierarchical', () { + final c = Logger('a.b.c'); + expect(c.name, equals('c')); + expect(c.parent!.name, equals('b')); + expect(c.parent!.parent!.name, equals('a')); + expect(c.parent!.parent!.parent!.name, equals('')); + expect(c.parent!.parent!.parent!.parent, isNull); + }); + + test('logger full name', () { + final c = Logger('a.b.c'); + expect(c.fullName, equals('a.b.c')); + expect(c.parent!.fullName, equals('a.b')); + expect(c.parent!.parent!.fullName, equals('a')); + expect(c.parent!.parent!.parent!.fullName, equals('')); + expect(c.parent!.parent!.parent!.parent, isNull); + }); + + test('logger parent-child links are correct', () { + final a = Logger('a'); + final b = Logger('a.b'); + final c = Logger('a.c'); + expect(a, same(b.parent)); + expect(a, same(c.parent)); + expect(a.children['b'], same(b)); + expect(a.children['c'], same(c)); + }); + + test('loggers are singletons', () { + final a1 = Logger('a'); + final a2 = Logger('a'); + final b = Logger('a.b'); + final root = Logger.root; + expect(a1, same(a2)); + expect(a1, same(b.parent)); + expect(root, same(a1.parent)); + expect(root, same(Logger(''))); + }); + + test('cannot directly manipulate Logger.children', () { + final loggerAB = Logger('a.b'); + final loggerA = loggerAB.parent!; + + expect(loggerA.children['b'], same(loggerAB), reason: 'can read Children'); + + expect(() { + loggerAB.children['test'] = Logger('Fake1234'); + }, throwsUnsupportedError, reason: 'Children is read-only'); + }); + + test('stackTrace gets throw to LogRecord', () { + Logger.root.level = Level.INFO; + + final records = []; + + final sub = Logger.root.onRecord.listen(records.add); + + try { + throw UnsupportedError('test exception'); + } catch (error, stack) { + Logger.root.log(Level.SEVERE, 'severe', error, stack); + Logger.root.warning('warning', error, stack); + } + + Logger.root.log(Level.SHOUT, 'shout'); + + sub.cancel(); + + expect(records, hasLength(3)); + + final severe = records[0]; + expect(severe.message, 'severe'); + expect(severe.error is UnsupportedError, isTrue); + expect(severe.stackTrace is StackTrace, isTrue); + + final warning = records[1]; + expect(warning.message, 'warning'); + expect(warning.error is UnsupportedError, isTrue); + expect(warning.stackTrace is StackTrace, isTrue); + + final shout = records[2]; + expect(shout.message, 'shout'); + expect(shout.error, isNull); + expect(shout.stackTrace, isNull); + }); + + group('zone gets recorded to LogRecord', () { + test('root zone', () { + final root = Logger.root; + + final recordingZone = Zone.current; + final records = []; + root.onRecord.listen(records.add); + root.info('hello'); + + expect(records, hasLength(1)); + expect(records.first.zone, equals(recordingZone)); + }); + + test('child zone', () { + final root = Logger.root; + + late Zone recordingZone; + final records = []; + root.onRecord.listen(records.add); + + runZoned(() { + recordingZone = Zone.current; + root.info('hello'); + }); + + expect(records, hasLength(1)); + expect(records.first.zone, equals(recordingZone)); + }); + + test('custom zone', () { + final root = Logger.root; + + late Zone recordingZone; + final records = []; + root.onRecord.listen(records.add); + + runZoned(() { + recordingZone = Zone.current; + }); + + runZoned(() => root.log(Level.INFO, 'hello', null, null, recordingZone)); + + expect(records, hasLength(1)); + expect(records.first.zone, equals(recordingZone)); + }); + }); + + group('detached loggers', () { + tearDown(() { + hierarchicalLoggingEnabled = hierarchicalLoggingEnabledDefault; + Logger.root.level = defaultLevel; + }); + + test('create new instances of Logger', () { + final a1 = Logger.detached('a'); + final a2 = Logger.detached('a'); + final a = Logger('a'); + + expect(a1, isNot(a2)); + expect(a1, isNot(a)); + expect(a2, isNot(a)); + }); + + test('parent is null', () { + final a = Logger.detached('a'); + expect(a.parent, null); + }); + + test('children is empty', () { + final a = Logger.detached('a'); + expect(a.children, {}); + }); + + test('have levels independent of the root level', () { + void testDetachedLoggerLevel(bool withHierarchy) { + hierarchicalLoggingEnabled = withHierarchy; + + const newRootLevel = Level.ALL; + const newDetachedLevel = Level.OFF; + + Logger.root.level = newRootLevel; + + final detached = Logger.detached('a'); + expect(detached.level, defaultLevel); + expect(Logger.root.level, newRootLevel); + + detached.level = newDetachedLevel; + expect(detached.level, newDetachedLevel); + expect(Logger.root.level, newRootLevel); + } + + testDetachedLoggerLevel(false); + testDetachedLoggerLevel(true); + }); + + test('log messages regardless of hierarchy', () { + void testDetachedLoggerOnRecord(bool withHierarchy) { + var calls = 0; + void handler(_) => calls += 1; + + hierarchicalLoggingEnabled = withHierarchy; + + final detached = Logger.detached('a'); + detached.level = Level.ALL; + detached.onRecord.listen(handler); + + Logger.root.info('foo'); + expect(calls, 0); + + detached.info('foo'); + detached.info('foo'); + expect(calls, 2); + } + + testDetachedLoggerOnRecord(false); + testDetachedLoggerOnRecord(true); + }); + }); + + group('mutating levels', () { + final root = Logger.root; + final a = Logger('a'); + final b = Logger('a.b'); + final c = Logger('a.b.c'); + final d = Logger('a.b.c.d'); + final e = Logger('a.b.c.d.e'); + + setUp(() { + hierarchicalLoggingEnabled = true; + root.level = Level.INFO; + a.level = null; + b.level = null; + c.level = null; + d.level = null; + e.level = null; + root.clearListeners(); + a.clearListeners(); + b.clearListeners(); + c.clearListeners(); + d.clearListeners(); + e.clearListeners(); + hierarchicalLoggingEnabled = false; + root.level = Level.INFO; + }); + + test('cannot set level if hierarchy is disabled', () { + expect(() => a.level = Level.FINE, throwsUnsupportedError); + }); + + test('cannot set the level to null on the root logger', () { + expect(() => root.level = null, throwsUnsupportedError); + }); + + test('cannot set the level to null on a detached logger', () { + expect(() => Logger.detached('l').level = null, throwsUnsupportedError); + }); + + test('loggers effective level - no hierarchy', () { + expect(root.level, equals(Level.INFO)); + expect(a.level, equals(Level.INFO)); + expect(b.level, equals(Level.INFO)); + + root.level = Level.SHOUT; + + expect(root.level, equals(Level.SHOUT)); + expect(a.level, equals(Level.SHOUT)); + expect(b.level, equals(Level.SHOUT)); + }); + + test('loggers effective level - with hierarchy', () { + hierarchicalLoggingEnabled = true; + expect(root.level, equals(Level.INFO)); + expect(a.level, equals(Level.INFO)); + expect(b.level, equals(Level.INFO)); + expect(c.level, equals(Level.INFO)); + + root.level = Level.SHOUT; + b.level = Level.FINE; + + expect(root.level, equals(Level.SHOUT)); + expect(a.level, equals(Level.SHOUT)); + expect(b.level, equals(Level.FINE)); + expect(c.level, equals(Level.FINE)); + }); + + test('loggers effective level - with changing hierarchy', () { + hierarchicalLoggingEnabled = true; + d.level = Level.SHOUT; + hierarchicalLoggingEnabled = false; + + expect(root.level, Level.INFO); + expect(d.level, root.level); + expect(e.level, root.level); + }); + + test('isLoggable is appropriate', () { + hierarchicalLoggingEnabled = true; + root.level = Level.SEVERE; + c.level = Level.ALL; + e.level = Level.OFF; + + expect(root.isLoggable(Level.SHOUT), isTrue); + expect(root.isLoggable(Level.SEVERE), isTrue); + expect(root.isLoggable(Level.WARNING), isFalse); + expect(c.isLoggable(Level.FINEST), isTrue); + expect(c.isLoggable(Level.FINE), isTrue); + expect(e.isLoggable(Level.SHOUT), isFalse); + }); + + test('add/remove handlers - no hierarchy', () { + var calls = 0; + void handler(_) { + calls++; + } + + final sub = c.onRecord.listen(handler); + root.info('foo'); + root.info('foo'); + expect(calls, equals(2)); + sub.cancel(); + root.info('foo'); + expect(calls, equals(2)); + }); + + test('add/remove handlers - with hierarchy', () { + hierarchicalLoggingEnabled = true; + var calls = 0; + void handler(_) { + calls++; + } + + c.onRecord.listen(handler); + root.info('foo'); + root.info('foo'); + expect(calls, equals(0)); + }); + + test('logging methods store appropriate level', () { + root.level = Level.ALL; + final rootMessages = []; + root.onRecord.listen((record) { + rootMessages.add('${record.level}: ${record.message}'); + }); + + root.finest('1'); + root.finer('2'); + root.fine('3'); + root.config('4'); + root.info('5'); + root.warning('6'); + root.severe('7'); + root.shout('8'); + + expect( + rootMessages, + equals([ + 'FINEST: 1', + 'FINER: 2', + 'FINE: 3', + 'CONFIG: 4', + 'INFO: 5', + 'WARNING: 6', + 'SEVERE: 7', + 'SHOUT: 8' + ])); + }); + + test('logging methods store exception', () { + root.level = Level.ALL; + final rootMessages = []; + root.onRecord.listen((r) { + rootMessages.add('${r.level}: ${r.message} ${r.error}'); + }); + + root.finest('1'); + root.finer('2'); + root.fine('3'); + root.config('4'); + root.info('5'); + root.warning('6'); + root.severe('7'); + root.shout('8'); + root.finest('1', 'a'); + root.finer('2', 'b'); + root.fine('3', ['c']); + root.config('4', 'd'); + root.info('5', 'e'); + root.warning('6', 'f'); + root.severe('7', 'g'); + root.shout('8', 'h'); + + expect( + rootMessages, + equals([ + 'FINEST: 1 null', + 'FINER: 2 null', + 'FINE: 3 null', + 'CONFIG: 4 null', + 'INFO: 5 null', + 'WARNING: 6 null', + 'SEVERE: 7 null', + 'SHOUT: 8 null', + 'FINEST: 1 a', + 'FINER: 2 b', + 'FINE: 3 [c]', + 'CONFIG: 4 d', + 'INFO: 5 e', + 'WARNING: 6 f', + 'SEVERE: 7 g', + 'SHOUT: 8 h' + ])); + }); + + test('message logging - no hierarchy', () { + root.level = Level.WARNING; + final rootMessages = []; + final aMessages = []; + final cMessages = []; + c.onRecord.listen((record) { + cMessages.add('${record.level}: ${record.message}'); + }); + a.onRecord.listen((record) { + aMessages.add('${record.level}: ${record.message}'); + }); + root.onRecord.listen((record) { + rootMessages.add('${record.level}: ${record.message}'); + }); + + root.info('1'); + root.fine('2'); + root.shout('3'); + + b.info('4'); + b.severe('5'); + b.warning('6'); + b.fine('7'); + + c.fine('8'); + c.warning('9'); + c.shout('10'); + + expect( + rootMessages, + equals([ + // 'INFO: 1' is not loggable + // 'FINE: 2' is not loggable + 'SHOUT: 3', + // 'INFO: 4' is not loggable + 'SEVERE: 5', + 'WARNING: 6', + // 'FINE: 7' is not loggable + // 'FINE: 8' is not loggable + 'WARNING: 9', + 'SHOUT: 10' + ])); + + // no hierarchy means we all hear the same thing. + expect(aMessages, equals(rootMessages)); + expect(cMessages, equals(rootMessages)); + }); + + test('message logging - with hierarchy', () { + hierarchicalLoggingEnabled = true; + + b.level = Level.WARNING; + + final rootMessages = []; + final aMessages = []; + final cMessages = []; + c.onRecord.listen((record) { + cMessages.add('${record.level}: ${record.message}'); + }); + a.onRecord.listen((record) { + aMessages.add('${record.level}: ${record.message}'); + }); + root.onRecord.listen((record) { + rootMessages.add('${record.level}: ${record.message}'); + }); + + root.info('1'); + root.fine('2'); + root.shout('3'); + + b.info('4'); + b.severe('5'); + b.warning('6'); + b.fine('7'); + + c.fine('8'); + c.warning('9'); + c.shout('10'); + + expect( + rootMessages, + equals([ + 'INFO: 1', + // 'FINE: 2' is not loggable + 'SHOUT: 3', + // 'INFO: 4' is not loggable + 'SEVERE: 5', + 'WARNING: 6', + // 'FINE: 7' is not loggable + // 'FINE: 8' is not loggable + 'WARNING: 9', + 'SHOUT: 10' + ])); + + expect( + aMessages, + equals([ + // 1,2 and 3 are lower in the hierarchy + // 'INFO: 4' is not loggable + 'SEVERE: 5', + 'WARNING: 6', + // 'FINE: 7' is not loggable + // 'FINE: 8' is not loggable + 'WARNING: 9', + 'SHOUT: 10' + ])); + + expect( + cMessages, + equals([ + // 1 - 7 are lower in the hierarchy + // 'FINE: 8' is not loggable + 'WARNING: 9', + 'SHOUT: 10' + ])); + }); + + test('message logging - lazy functions', () { + root.level = Level.INFO; + final messages = []; + root.onRecord.listen((record) { + messages.add('${record.level}: ${record.message}'); + }); + + var callCount = 0; + String myClosure() => '${++callCount}'; + + root.info(myClosure); + root.finer(myClosure); // Should not get evaluated. + root.warning(myClosure); + + expect( + messages, + equals([ + 'INFO: 1', + 'WARNING: 2', + ])); + }); + + test('message logging - calls toString', () { + root.level = Level.INFO; + final messages = []; + final objects = []; + final object = Object(); + root.onRecord.listen((record) { + messages.add('${record.level}: ${record.message}'); + objects.add(record.object); + }); + + root.info(5); + root.info(false); + root.info([1, 2, 3]); + root.info(() => 10); + root.info(object); + + expect( + messages, + equals([ + 'INFO: 5', + 'INFO: false', + 'INFO: [1, 2, 3]', + 'INFO: 10', + "INFO: Instance of 'Object'" + ])); + + expect(objects, [ + 5, + false, + [1, 2, 3], + 10, + object + ]); + }); + }); + + group('recordStackTraceAtLevel', () { + final root = Logger.root; + tearDown(() { + recordStackTraceAtLevel = Level.OFF; + root.clearListeners(); + }); + + test('no stack trace by default', () { + final records = []; + root.onRecord.listen(records.add); + root.severe('hello'); + root.warning('hello'); + root.info('hello'); + expect(records, hasLength(3)); + expect(records[0].stackTrace, isNull); + expect(records[1].stackTrace, isNull); + expect(records[2].stackTrace, isNull); + }); + + test('trace recorded only on requested levels', () { + final records = []; + recordStackTraceAtLevel = Level.WARNING; + root.onRecord.listen(records.add); + root.severe('hello'); + root.warning('hello'); + root.info('hello'); + expect(records, hasLength(3)); + expect(records[0].stackTrace, isNotNull); + expect(records[1].stackTrace, isNotNull); + expect(records[2].stackTrace, isNull); + }); + + test('defaults a missing trace', () { + final records = []; + recordStackTraceAtLevel = Level.SEVERE; + root.onRecord.listen(records.add); + root.severe('hello'); + expect(records.single.stackTrace, isNotNull); + }); + + test('defaults an empty trace', () { + final records = []; + recordStackTraceAtLevel = Level.SEVERE; + root.onRecord.listen(records.add); + root.severe('hello', 'error', StackTrace.empty); + expect(records.single.stackTrace, isNot(StackTrace.empty)); + }); + + test('provided trace is used if given', () { + final trace = StackTrace.current; + final records = []; + recordStackTraceAtLevel = Level.WARNING; + root.onRecord.listen(records.add); + root.severe('hello'); + root.warning('hello', 'a', trace); + expect(records, hasLength(2)); + expect(records[0].stackTrace, isNot(equals(trace))); + expect(records[1].stackTrace, trace); + }); + + test('error also generated when generating a trace', () { + final records = []; + recordStackTraceAtLevel = Level.WARNING; + root.onRecord.listen(records.add); + root.severe('hello'); + root.warning('hello'); + root.info('hello'); + expect(records, hasLength(3)); + expect(records[0].error, isNotNull); + expect(records[1].error, isNotNull); + expect(records[2].error, isNull); + }); + + test('listen for level changed', () { + final levels = []; + root.level = Level.ALL; + root.onLevelChanged.listen(levels.add); + root.level = Level.SEVERE; + root.level = Level.WARNING; + expect(levels, hasLength(2)); + }); + + test('onLevelChanged is not emited if set the level to the same value', () { + final levels = []; + root.level = Level.ALL; + root.onLevelChanged.listen(levels.add); + root.level = Level.ALL; + expect(levels, hasLength(0)); + }); + + test('setting level in a loop throws state error', () { + root.level = Level.ALL; + root.onLevelChanged.listen((event) { + // Cannot fire new event. Controller is already firing an event + expect(() => root.level = Level.SEVERE, throwsStateError); + }); + root.level = Level.WARNING; + expect(root.level, Level.SEVERE); + }); + }); +} diff --git a/pkgs/os_detect/.gitignore b/pkgs/os_detect/.gitignore new file mode 100644 index 00000000..49ce72d7 --- /dev/null +++ b/pkgs/os_detect/.gitignore @@ -0,0 +1,3 @@ +.dart_tool/ +.packages +pubspec.lock diff --git a/pkgs/os_detect/AUTHORS b/pkgs/os_detect/AUTHORS new file mode 100644 index 00000000..846e4a15 --- /dev/null +++ b/pkgs/os_detect/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Dart project. Names should be added to the list like so: +# +# Name/Organization + +Google LLC diff --git a/pkgs/os_detect/CHANGELOG.md b/pkgs/os_detect/CHANGELOG.md new file mode 100644 index 00000000..0dea3e5d --- /dev/null +++ b/pkgs/os_detect/CHANGELOG.md @@ -0,0 +1,17 @@ +## 2.0.2 + +- Require Dart 3.0 +- Make work with VM's platform-constants. +- Move to `dart-lang/core` monorepo. + +## 2.0.1 + +- Populate the pubspec `repository` field. + +## 2.0.0 + +- Stable null safety release. + +## 1.0.0 + +- Initial release diff --git a/pkgs/os_detect/LICENSE b/pkgs/os_detect/LICENSE new file mode 100644 index 00000000..ed0a3506 --- /dev/null +++ b/pkgs/os_detect/LICENSE @@ -0,0 +1,27 @@ +Copyright 2020, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/os_detect/README.md b/pkgs/os_detect/README.md new file mode 100644 index 00000000..cb931b15 --- /dev/null +++ b/pkgs/os_detect/README.md @@ -0,0 +1,42 @@ +[![Dart CI](https://github.com/dart-lang/core/actions/workflows/os_detect.yaml/badge.svg)](https://github.com/dart-lang/core/actions/workflows/os_detect.yaml) +[![pub package](https://img.shields.io/pub/v/os_detect.svg)](https://pub.dev/packages/os_detect) +[![package publisher](https://img.shields.io/pub/publisher/os_detect.svg)](https://pub.dev/packages/os_detect/publisher) + +Platform independent access to information about the current operating system. + +## Querying the current OS + +Exposes `operatingSystem` and `operatingSystemVersion` strings similar to those +of the `Platform` class in `dart:io`, but also works on the web. The +`operatingSystem` of a browser is the string "browser". Also exposes convenience +getters like `isLinux`, `isAndroid` and `isBrowser` based on the +`operatingSystem` string. + +To use this package instead of `dart:io`, replace the import of `dart:io` with: + +```dart +import 'package:os_detect/os_detect.dart' as os_detect; +``` + +That should keep the code working if the only functionality used from `dart:io` +is operating system detection. You should then use your IDE to rename the import +prefix from `Platform` to something lower-cased which follows the style guide +for import prefixes. + +Any new platform which supports neither `dart:io` nor `dart:html` can make +itself recognizable by configuring the `dart.os.name` and `dart.os.version` +environment settings, so that `const String.fromEnvironment` can access them. + +## Overriding the current OS string + +It's possible to override the current operating system string, as exposed by +`operatingSystem` and `operatingSystemVersion` in +`package:os_detect/os_detect.dart`. To do so, import the +`package:os_detect/override.dart` library and use the `overrideOperatingSystem` +function to run code in a zone where the operating system and version values are +set to whatever values are desired. + +The class `OperatingSystemID` can also be used directly to abstract over the +operating system name and version. The `OperatingSystemID.current` defaults to +the values provided by the platform when not overridden using +`overrideOperatingSystem`. diff --git a/pkgs/os_detect/analysis_options.yaml b/pkgs/os_detect/analysis_options.yaml new file mode 100644 index 00000000..f29baf1b --- /dev/null +++ b/pkgs/os_detect/analysis_options.yaml @@ -0,0 +1,27 @@ +# https://dart.dev/tools/analysis#the-analysis-options-file +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_unused_constructor_parameters + - avoid_void_async + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - package_api_docs + - prefer_const_declarations + - use_raw_strings + + diff --git a/pkgs/os_detect/bin/os_detect.dart b/pkgs/os_detect/bin/os_detect.dart new file mode 100644 index 00000000..e9e6fc15 --- /dev/null +++ b/pkgs/os_detect/bin/os_detect.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Prints the operating system detected by the current compilation environment. +library pkg.os_detect.run; + +import 'package:os_detect/os_detect.dart' as os_detect; + +void main() { + final knownName = knownOSName(); + print('OS name : ${os_detect.operatingSystem} ' + '${knownName != null ? '($knownName)' : ''}'); + print('OS version : ${os_detect.operatingSystemVersion}'); +} + +String? knownOSName() { + if (os_detect.isAndroid) { + return 'Android'; + } + if (os_detect.isBrowser) { + return 'Browser'; + } + if (os_detect.isFuchsia) { + return 'Fuchsia'; + } + if (os_detect.isIOS) { + return 'iOS'; + } + if (os_detect.isLinux) { + return 'Linux'; + } + if (os_detect.isMacOS) { + return 'MacOS'; + } + if (os_detect.isWindows) { + return 'Windows'; + } + return null; +} diff --git a/pkgs/os_detect/example/example.dart b/pkgs/os_detect/example/example.dart new file mode 100644 index 00000000..4a159d86 --- /dev/null +++ b/pkgs/os_detect/example/example.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:os_detect/os_detect.dart' as os_detect; + +void main() { + print(''' + OS ID: ${os_detect.operatingSystem} +OS Version: ${os_detect.operatingSystemVersion}'''); + if (os_detect.isAndroid) { + print(' OS Type: Android'); + } else if (os_detect.isBrowser) { + print(' OS Type: Browser'); + } else if (os_detect.isFuchsia) { + print(' OS Type: Fuchsia'); + } else if (os_detect.isIOS) { + print(' OS Type: iOS'); + } else if (os_detect.isLinux) { + print(' OS Type: Linux'); + } else if (os_detect.isMacOS) { + print(' OS Type: MacOS'); + } else if (os_detect.isWindows) { + print(' OS Type: Windows'); + } +} diff --git a/pkgs/os_detect/example/tree_shaking.dart b/pkgs/os_detect/example/tree_shaking.dart new file mode 100644 index 00000000..987f3ddb --- /dev/null +++ b/pkgs/os_detect/example/tree_shaking.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Try compiling this example with (if on Linux): +// +// dart compile exe --target-os=linux tree_shaking.dart +// +// then check that "SOMETHING ELSE" does not occur in the +// output `tree_shaking.exe` program, e.g.: +// +// strings tree_shaking.exe | grep SOMETHING +// +// which shows no matches. + +import 'package:os_detect/os_detect.dart' as platform; + +void main() { + if (platform.isLinux) { + print('Is Linux'); + } else { + print('SOMETHING ELSE'); + } + if (platform.operatingSystem == 'linux') { + print('Is Linux'); + } else { + print('SOMETHING ELSE'); + } +} diff --git a/pkgs/os_detect/lib/os_detect.dart b/pkgs/os_detect/lib/os_detect.dart new file mode 100644 index 00000000..d323f63a --- /dev/null +++ b/pkgs/os_detect/lib/os_detect.dart @@ -0,0 +1,104 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Information about the current operating system. +library pkg.os_detect; + +import 'src/os_override.dart'; + +/// Identification of the current operating system or platform. +/// +/// Specific known operating systems are reported by a unique known string, +/// and all the `is` values are computed by comparing the +/// [operatingSystem] string against those known strings. +/// That means that *at most* one of those value can be `true`, +/// and usually precisely one will be `true`. +/// +/// **Notice:** Programs running in a browser will report their +/// operating system as `"browser"`, not the operating system +/// that browser is running on. See [isBrowser]. +String get operatingSystem => OperatingSystem.current.id; + +/// Representation of the version of the current operating system or platform. +/// +/// May be empty if no version is known or available. +String get operatingSystemVersion => OperatingSystem.current.version; + +/// Whether the current operating system is a version of +/// [Linux](https://en.wikipedia.org/wiki/Linux). +/// +/// Identified by [operatingSystem] being the string `linux`. +/// +/// This value is `false` if the operating system is a specialized +/// version of Linux that identifies itself by a different name, +/// for example Android (see [isAndroid]), +/// or if the code is running inside a browser (see [isBrowser]). +@pragma('vm:prefer-inline') +bool get isLinux => OperatingSystem.current.isLinux; + +/// Whether the current operating system is a version of +/// [macOS](https://en.wikipedia.org/wiki/MacOS). +/// +/// Identified by [operatingSystem] being the string `macos`. +/// +/// The value is `false` if the code is running inside a browser, +/// even if that browser is running on MacOS (see [isBrowser]). +@pragma('vm:prefer-inline') +bool get isMacOS => OperatingSystem.current.isMacOS; + +/// Whether the current operating system is a version of +/// [Microsoft Windows](https://en.wikipedia.org/wiki/Microsoft_Windows). +/// +/// Identified by [operatingSystem] being the string `windows`. +/// +/// The value is `false` if the code is running inside a browser, +/// even if that browser is running on Windows (see [isBrowser]). +@pragma('vm:prefer-inline') +bool get isWindows => OperatingSystem.current.isWindows; + +/// Whether the current operating system is a version of +/// [Android](https://en.wikipedia.org/wiki/Android_%28operating_system%29). +/// +/// Identified by [operatingSystem] being the string `android`. +/// +/// The value is `false` if the code is running inside a browser, +/// even if that browser is running on Android (see [isBrowser]). +@pragma('vm:prefer-inline') +bool get isAndroid => OperatingSystem.current.isAndroid; + +/// Whether the current operating system is a version of +/// [iOS](https://en.wikipedia.org/wiki/IOS). +/// +/// Identified by [operatingSystem] being the string `ios`. +/// +/// The value is `false` if the code is running inside a browser, +/// even if that browser is running on iOS (see [isBrowser]). +@pragma('vm:prefer-inline') +bool get isIOS => OperatingSystem.current.isIOS; + +/// Whether the current operating system is a version of +/// [Fuchsia](https://en.wikipedia.org/wiki/Google_Fuchsia). +/// +/// Identified by [operatingSystem] being the string `fuchsia`. +/// +/// The value is `false` if the code is running inside a browser, +/// even if that browser is running on Fuchsia (see [isBrowser]). +@pragma('vm:prefer-inline') +bool get isFuchsia => OperatingSystem.current.isFuchsia; + +/// Whether running in a web browser. +/// +/// Identified by [operatingSystem] being the string `browser`. +/// +/// If so, the [operatingSystemVersion] is the string made available +/// through `window.navigator.appVersion`. +/// +/// The value is `true` when the code is running inside a browser, +/// no matter which operating system the browser is itself running on. +/// No attempt is made to detect the underlying operating system. +/// That information *may* be derived from [operatingSystemVersion], +/// but browsers are able to lie in the app-version/user-agent +/// string. +@pragma('vm:prefer-inline') +bool get isBrowser => OperatingSystem.current.isBrowser; diff --git a/pkgs/os_detect/lib/override.dart b/pkgs/os_detect/lib/override.dart new file mode 100644 index 00000000..cc3e9188 --- /dev/null +++ b/pkgs/os_detect/lib/override.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Functionality to override information about the current platform. +library; + +export 'src/os_override.dart' show OperatingSystem, overrideOperatingSystem; diff --git a/pkgs/os_detect/lib/src/os_kind.dart b/pkgs/os_detect/lib/src/os_kind.dart new file mode 100644 index 00000000..7f4ee852 --- /dev/null +++ b/pkgs/os_detect/lib/src/os_kind.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Shared constants and classes used to represent a recongized OS type +/// +/// Not exported in the public API, but used to communicate between +/// `override.dart` and the conditionally imported `osid_X.dart` files. +/// +/// When the platform is statically known, all but one of the subclasses +/// should be tree-shaken, so an `os is AndroidOS` can be resolved to +/// a constant true/false depending on whether the class is the retained one +/// or not. +library; + +/// Operating identity object. +/// +/// By only instantiating these subtypes guarded by target-OS guarded +/// checks, unless using the "for testing" `OperatingSystem` constructor, +/// all but one of the subclasses should be tree-shaken, +/// and, e.g., the `_isId is IOS` test above should become tree-shakable +/// on all other platforms. +sealed class RecognizedOS { + // The recognized OS identifier strings recognized. + static const androidId = 'android'; + static const browserId = 'browser'; + static const fuchsiaId = 'fuchsia'; + static const iOSId = 'ios'; + static const linuxId = 'linux'; + static const macOSId = 'macos'; + static const windowsId = 'windows'; + + abstract final String id; + const RecognizedOS(); +} + +/// Operations system object for Android. +class AndroidOS extends RecognizedOS { + @override + final String id = RecognizedOS.androidId; + const AndroidOS(); +} + +/// Operations system object for browsers. +class BrowserOS extends RecognizedOS { + @override + final String id = RecognizedOS.browserId; + const BrowserOS(); +} + +/// Operations system object for Fuchsia. +class FuchsiaOS extends RecognizedOS { + @override + final String id = RecognizedOS.fuchsiaId; + const FuchsiaOS(); +} + +/// Operations system object for iOS. +class IOS extends RecognizedOS { + @override + final String id = RecognizedOS.iOSId; + const IOS(); +} + +/// Operations system object for Linux. +class LinuxOS extends RecognizedOS { + @override + final String id = RecognizedOS.linuxId; + const LinuxOS(); +} + +/// Operations system object for MacOS. +class MacOS extends RecognizedOS { + @override + final String id = RecognizedOS.macOSId; + const MacOS(); +} + +/// Operations system object for Windows. +class WindowsOS extends RecognizedOS { + @override + final String id = RecognizedOS.windowsId; + const WindowsOS(); +} + +/// Fallback to represent unknown operating system. +/// +/// Do not use for one of the recognized operating +/// systems +class UnknownOS extends RecognizedOS { + @override + final String id; + const UnknownOS(this.id) + : assert(id != RecognizedOS.linuxId), + assert(id != RecognizedOS.macOSId), + assert(id != RecognizedOS.windowsId), + assert(id != RecognizedOS.androidId), + assert(id != RecognizedOS.iOSId), + assert(id != RecognizedOS.fuchsiaId), + assert(id != RecognizedOS.browserId); +} diff --git a/pkgs/os_detect/lib/src/os_override.dart b/pkgs/os_detect/lib/src/os_override.dart new file mode 100644 index 00000000..6f31bfd4 --- /dev/null +++ b/pkgs/os_detect/lib/src/os_override.dart @@ -0,0 +1,180 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async' show Zone, runZoned; + +import 'package:meta/meta.dart'; + +import 'os_kind.dart'; +import 'osid_unknown.dart' + if (dart.library.io) 'osid_io.dart' + if (dart.library.html) 'osid_html.dart'; + +/// The name and version of an operating system. +final class OperatingSystem { + // The recognized OS identifier strings. + + /// The operating system ID string for Linux. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const androidId = RecognizedOS.androidId; + + /// The operating system ID string for browsers. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const browserId = RecognizedOS.browserId; + + /// The operating system ID string for Fuchsia. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const fuchsiaId = RecognizedOS.fuchsiaId; + + /// The operating system ID string for iOS. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const iOSId = RecognizedOS.iOSId; + + /// The operating system ID string for Linux. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const linuxId = RecognizedOS.linuxId; + + /// The operating system ID string for macOS. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const macOSId = RecognizedOS.macOSId; + + /// The operating system ID string for Windows. + /// + /// Compare against [id] or the `operatingSystem` of `os_detect.dart`, + /// or use as argument to [OperatingSystem.new]. + static const windowsId = RecognizedOS.windowsId; + + /// The current operating system ID. + /// + /// Defaults to what information is available + /// from known platform specific libraries, + /// but can be overridden using functionality from the + /// `osid_override.dart` library. + @pragma('vm:try-inline') + static OperatingSystem get current => + Zone.current[#_os] as OperatingSystem? ?? platformOS; + + /// A string representing the operating system or platform. + String get id => _osId.id; + + // Operating system ID object. + final RecognizedOS _osId; + + /// A string representing the version of the operating system or platform. + /// + /// May be empty if no version is known or available. + final String version; + + /// Creates a new operating system object for testing. + /// + /// Can be used with [overrideOperatingSystem] to selectively + /// change the value returned by [current]. + /// + /// **Notice:** Using this constructor may reduce the efficiency + /// of compilers recognizing code that isn't needed when compiling + /// for a particular platform (aka. "tree-shaking" of unreachable code). + // Uses chained conditionals to allow back-ends to constant fold when they + // know what `id` is, which they'd usually know for a specific operation. + // That can avoid retaining *all* the subclasses of `OS`. + @visibleForTesting + @pragma('vm:prefer-inline') + OperatingSystem(String id, String version) + : this._( + id == linuxId + ? const LinuxOS() + : id == macOSId + ? const MacOS() + : id == windowsId + ? const WindowsOS() + : id == androidId + ? const AndroidOS() + : id == iOSId + ? const IOS() + : id == fuchsiaId + ? const FuchsiaOS() + : id == browserId + ? const BrowserOS() + : UnknownOS(id), + version); + + /// Used by platforms which know the ID object. + const OperatingSystem._(this._osId, this.version); + + /// Whether the operating system is a version of + /// [Linux](https://en.wikipedia.org/wiki/Linux). + /// + /// Identified by [id] being the string `linux`. + /// + /// This value is `false` if the operating system is a specialized + /// version of Linux that identifies itself by a different name, + /// for example Android (see [isAndroid]). + bool get isLinux => _osId is LinuxOS; + + /// Whether the operating system is a version of + /// [macOS](https://en.wikipedia.org/wiki/MacOS). + /// + /// Identified by [id] being the string `macos`. + bool get isMacOS => _osId is MacOS; + + /// Whether the operating system is a version of + /// [Microsoft Windows](https://en.wikipedia.org/wiki/Microsoft_Windows). + /// + /// Identified by [id] being the string `windows`. + bool get isWindows => _osId is WindowsOS; + + /// Whether the operating system is a version of + /// [Android](https://en.wikipedia.org/wiki/Android_%28operating_system%29). + /// + /// Identified by [id] being the string `android`. + bool get isAndroid => _osId is AndroidOS; + + /// Whether the operating system is a version of + /// [iOS](https://en.wikipedia.org/wiki/IOS). + /// + /// Identified by [id] being the string `ios`. + bool get isIOS => _osId is IOS; + + /// Whether the operating system is a version of + /// [Fuchsia](https://en.wikipedia.org/wiki/Google_Fuchsia). + /// + /// Identified by [id] being the string `fuchsia`. + bool get isFuchsia => _osId is FuchsiaOS; + + /// Whether running in a web browser. + /// + /// Identified by [id] being the string `browser`. + /// + /// If so, the [version] is the string made available + /// through `window.navigator.appVersion`. + bool get isBrowser => _osId is BrowserOS; +} + +/// Run [body] in a zone with platform overrides. +/// +/// Overrides [OperatingSystem.current] with the supplied [operatingSystem] +/// value while running in a new zone, and then runs [body] in that zone. +/// +/// This override affects the `operatingSystem` and `version` +/// exported by `package:osid/osid.dart`. +R overrideOperatingSystem( + OperatingSystem operatingSystem, R Function() body) => + runZoned(body, zoneValues: {#_os: operatingSystem}); + +// Exposes the `OperatingSystem._` constructor to the conditionally imported +// libraries. Not exported by `../override.dart'. +final class OperatingSystemInternal extends OperatingSystem { + const OperatingSystemInternal(super.id, super.version) : super._(); +} diff --git a/pkgs/os_detect/lib/src/osid_html.dart b/pkgs/os_detect/lib/src/osid_html.dart new file mode 100644 index 00000000..a5d896fb --- /dev/null +++ b/pkgs/os_detect/lib/src/osid_html.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:html'; + +import 'os_kind.dart' show BrowserOS; +import 'os_override.dart'; + +String get _osVersion => window.navigator.appVersion; + +final OperatingSystem platformOS = + OperatingSystemInternal(const BrowserOS(), _osVersion); diff --git a/pkgs/os_detect/lib/src/osid_io.dart b/pkgs/os_detect/lib/src/osid_io.dart new file mode 100644 index 00000000..56b45eb9 --- /dev/null +++ b/pkgs/os_detect/lib/src/osid_io.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'os_kind.dart'; +import 'os_override.dart'; + +// Uses VM platform-constant functionality to constant fold this expression +// when `Platform.operatingSystem` is known at compile-time. +// Uses a valid "potentially constant" expression for this, instead of, e.g., +// a `switch` expression. +@pragma('vm:platform-const') +final RecognizedOS? _osType = Platform.operatingSystem == RecognizedOS.linuxId + ? const LinuxOS() + : Platform.operatingSystem == RecognizedOS.macOSId + ? const MacOS() + : Platform.operatingSystem == RecognizedOS.windowsId + ? const WindowsOS() + : Platform.operatingSystem == RecognizedOS.androidId + ? const AndroidOS() + : Platform.operatingSystem == RecognizedOS.iOSId + ? const IOS() + : Platform.operatingSystem == RecognizedOS.fuchsiaId + ? const FuchsiaOS() + : Platform.operatingSystem == RecognizedOS.browserId + ? const BrowserOS() + : null; + +final OperatingSystem platformOS = OperatingSystemInternal( + _osType ?? UnknownOS(Platform.operatingSystem), + Platform.operatingSystemVersion); diff --git a/pkgs/os_detect/lib/src/osid_unknown.dart b/pkgs/os_detect/lib/src/osid_unknown.dart new file mode 100644 index 00000000..2e6798ef --- /dev/null +++ b/pkgs/os_detect/lib/src/osid_unknown.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'os_kind.dart'; +import 'os_override.dart'; + +@pragma('vm:platform-const') +const String _os = + String.fromEnvironment('dart.os.name', defaultValue: 'unknown'); +const String _osVersion = String.fromEnvironment('dart.os.version'); + +const OperatingSystem platformOS = OperatingSystemInternal( + _os == RecognizedOS.linuxId + ? LinuxOS() + : _os == RecognizedOS.macOSId + ? MacOS() + : _os == RecognizedOS.windowsId + ? WindowsOS() + : _os == RecognizedOS.androidId + ? AndroidOS() + : _os == RecognizedOS.iOSId + ? IOS() + : _os == RecognizedOS.fuchsiaId + ? FuchsiaOS() + : _os == RecognizedOS.browserId + ? BrowserOS() + : UnknownOS(_os), + _osVersion); diff --git a/pkgs/os_detect/pubspec.yaml b/pkgs/os_detect/pubspec.yaml new file mode 100644 index 00000000..fb927b4c --- /dev/null +++ b/pkgs/os_detect/pubspec.yaml @@ -0,0 +1,14 @@ +name: os_detect +version: 2.0.2 +description: Platform independent OS detection. +repository: https://github.com/dart-lang/core/tree/main/pkgs/os_detect + +environment: + sdk: ^3.0.0 + +dependencies: + meta: ^1.9.0 + +dev_dependencies: + dart_flutter_team_lints: ^2.0.0 + test: ^1.24.0 diff --git a/pkgs/os_detect/test/osid_test.dart b/pkgs/os_detect/test/osid_test.dart new file mode 100644 index 00000000..862d9377 --- /dev/null +++ b/pkgs/os_detect/test/osid_test.dart @@ -0,0 +1,73 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:os_detect/os_detect.dart'; +import 'package:os_detect/override.dart'; +import 'package:test/test.dart'; + +void main() { + test('Exists and is consistent', () { + expect(operatingSystem, isNotNull); + expect(operatingSystemVersion, isNotNull); + + expect(isLinux, operatingSystem == OperatingSystem.linuxId); + expect(isAndroid, operatingSystem == OperatingSystem.androidId); + expect(isMacOS, operatingSystem == OperatingSystem.macOSId); + expect(isWindows, operatingSystem == OperatingSystem.windowsId); + expect(isIOS, operatingSystem == OperatingSystem.iOSId); + expect(isFuchsia, operatingSystem == OperatingSystem.fuchsiaId); + expect(isBrowser, operatingSystem == OperatingSystem.browserId); + }); + + test('Override', () { + const overrideName = 'argle-bargle'; + const overrideVersion = 'glop-glyf'; + final overrideOS = OperatingSystem(overrideName, overrideVersion); + Zone? overrideZone; + + final originalName = operatingSystem; + final originalVersion = operatingSystemVersion; + final originalID = OperatingSystem.current; + final originalZone = Zone.current; + expect(originalName, isNot(overrideName)); + expect(originalVersion, isNot(overrideVersion)); + + // Override OS ID. + overrideOperatingSystem(overrideOS, () { + overrideZone = Zone.current; + expect(operatingSystem, overrideName); + expect(operatingSystemVersion, overrideVersion); + expect(OperatingSystem.current, same(overrideOS)); + // Nested override. + overrideOperatingSystem(originalID, () { + expect(operatingSystem, originalName); + expect(operatingSystemVersion, originalVersion); + expect(OperatingSystem.current, same(originalID)); + }); + expect(operatingSystem, overrideName); + expect(operatingSystemVersion, overrideVersion); + expect(OperatingSystem.current, same(overrideOS)); + // Captured parent zone does not have override. + originalZone.run(() { + expect(operatingSystem, originalName); + expect(operatingSystemVersion, originalVersion); + }); + expect(operatingSystem, overrideName); + expect(operatingSystemVersion, overrideVersion); + expect(OperatingSystem.current, same(overrideOS)); + }); + + expect(operatingSystem, originalName); + expect(operatingSystemVersion, originalVersion); + + // A captured override zone retains the override. + overrideZone!.run(() { + expect(operatingSystem, overrideName); + expect(operatingSystemVersion, overrideVersion); + expect(OperatingSystem.current, same(overrideOS)); + }); + }); +}