diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a38b0e..3a654a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,8 @@ jobs: # Spec 0006 criterion 14a: an example that depends on `any` builds # against whatever is newest that day, so no example pubspec names - # `any`, or no constraint, under dependencies or dev_dependencies. + # `any`, or no constraint, under dependencies or dev_dependencies, and + # a git dependency pins a full commit hash. - name: Example constraints if: success() || failure() working-directory: tool diff --git a/AUTHORING.md b/AUTHORING.md index b6caf77..383dd24 100644 --- a/AUTHORING.md +++ b/AUTHORING.md @@ -293,6 +293,9 @@ watch deviations, each listed in the package `README.md` and - `Platform.isIOS` becomes `FlutterWatchosPlatform.isIos` where the call it guards reaches a plugin with no watchOS implementation. - A missing `*_watchos` package is added to the example's pubspec. +- A dependency that upstream's example takes from upstream's repository, + because it is not on pub.dev, is pinned with `ref` to the commit of the + upstream release the package is tested against. - An upstream comment with a forbidden word is reworded. If upstream's example cannot launch on the watch even with these deviations diff --git a/packages/sqflite_watchos/CHANGELOG.md b/packages/sqflite_watchos/CHANGELOG.md index bb63e56..d4f6237 100644 --- a/packages/sqflite_watchos/CHANGELOG.md +++ b/packages/sqflite_watchos/CHANGELOG.md @@ -22,6 +22,7 @@ physical watch. minor. * Two FFI exports: `sqflite_watchos_databases_path` and `sqflite_watchos_sqlite_version`. -* The example's `lib/` is a small watch demo, because sqflite's own example - depends on the unpublished `sqflite_example_common`; its official - integration test is upstream's, unchanged. +* The example is sqflite 2.4.5's own. It depends on `sqflite_example_common`, + which is not on pub.dev, so its pubspec pins that package to the + tekartik/sqflite commit of the release. Its official integration test is + upstream's, unchanged. diff --git a/packages/sqflite_watchos/PORTING_REPORT.md b/packages/sqflite_watchos/PORTING_REPORT.md index cb9b1f1..6052d4a 100644 --- a/packages/sqflite_watchos/PORTING_REPORT.md +++ b/packages/sqflite_watchos/PORTING_REPORT.md @@ -62,10 +62,29 @@ replaced by `Platform.operatingSystemVersion`. ## Example -sqflite's own example app is built on `sqflite_example_common`, which is -not published, so `example/lib/main.dart` is a small watch demo instead. -`example/integration_test/sqflite_test.dart` is upstream's test, unchanged but for -the formatting this repo's CI checks. +`example/` is sqflite 2.4.5's own example (`sqflite/example` in +tekartik/sqflite): `lib/main.dart` and `lib/compat_test_run.dart` are +upstream's, unchanged. Its pages come from `sqflite_example_common`, which is +not published on pub.dev. The watch deviations are both in +`example/pubspec.yaml`: + +| Change | Why | +|---|---| +| `sqflite_example_common` is pinned with `ref` to tekartik/sqflite `21d2b8d` | it is not on pub.dev, and upstream's pubspec takes it from the repository's default branch, which can move under the example; `21d2b8d` is the commit of sqflite 2.4.5 | +| `sqflite_watchos` is added | the watch implementation | + +The other dependencies are upstream's, each with a caret on the version it +ran with here, as this repository's CI asks. +`example/integration_test/sqflite_test.dart` is upstream's test, unchanged +but for the formatting this repo's CI checks. + +On the watchOS 27.0 Simulator (Series 12, 46mm, 2026-10-30) the example +started with no error in its console (first frame 0.8 s after the engine +ran, built with `--dart-define=flutter.inspector.structuredErrors=false`), +and its list of test pages fits the screen. Its "Raw tests" page ran +upstream's SQL checks, against SQLite 3.54.0: every result seen while +scrolling the page was a pass, and the console showed no failure. The +official integration test passed (14 tests). ## Protocol coverage diff --git a/packages/sqflite_watchos/example/README.md b/packages/sqflite_watchos/example/README.md index 69ed5b7..9ff8e0a 100644 --- a/packages/sqflite_watchos/example/README.md +++ b/packages/sqflite_watchos/example/README.md @@ -1,15 +1,18 @@ # sqflite_example -A watchOS-only example: laps saved to a SQLite database on the watch. Run it -on an Apple Watch simulator with: +sqflite 2.4.5's own example app (`sqflite/example`), on watchOS: the test +pages of `sqflite_example_common`, which is not on pub.dev, so the pubspec +takes it from tekartik/sqflite at the commit of that release. Run it on an +Apple Watch simulator with: ```sh flutter-watchos run ``` `integration_test/sqflite_test.dart` is sqflite's official integration test, -unchanged. It uses `package:test`'s `test`, which `flutter-watchos drive` -does not collect, so run it with `test`: +unchanged but for the formatting this repository's CI checks. It uses +`package:test`'s `test`, which `flutter-watchos drive` does not collect, so +run it with `test`: ```sh flutter-watchos test integration_test/sqflite_test.dart -d diff --git a/packages/sqflite_watchos/example/lib/compat_test_run.dart b/packages/sqflite_watchos/example/lib/compat_test_run.dart new file mode 100644 index 0000000..2227cc9 --- /dev/null +++ b/packages/sqflite_watchos/example/lib/compat_test_run.dart @@ -0,0 +1,29 @@ +import 'package:sqflite/sqflite.dart'; + +import 'package:sqflite_example_common/database/database.dart'; + +/// Test compatibility old API. +Future testDebugModeCompat() async { + //await Sqflite.devSetDebugModeOn(false); + final path = await initDeleteDb('debug_mode.db'); + final db = await openDatabase(path); + try { + // ignore: deprecated_member_use + final debugModeOn = await Sqflite.getDebugModeOn(); + // ignore: deprecated_member_use + await Sqflite.setDebugModeOn(true); + await db.setVersion(1); + // ignore: deprecated_member_use + await Sqflite.setDebugModeOn(false); + // this message should not appear + await db.setVersion(2); + // ignore: deprecated_member_use + await Sqflite.setDebugModeOn(true); + await db.setVersion(3); + // restore + // ignore: deprecated_member_use + await Sqflite.setDebugModeOn(debugModeOn); + } finally { + await db.close(); + } +} diff --git a/packages/sqflite_watchos/example/lib/main.dart b/packages/sqflite_watchos/example/lib/main.dart index 292ec95..d056c3c 100644 --- a/packages/sqflite_watchos/example/lib/main.dart +++ b/packages/sqflite_watchos/example/lib/main.dart @@ -1,112 +1,6 @@ -// Copyright 2026 The FlutterWatch Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// -// sqflite's own example app is built on sqflite_example_common, which is not -// published, so this watch-sized demo stands in for it. The official -// integration test in integration_test/ is upstream's, unchanged. +import 'package:sqflite_example_common/main.dart'; -import 'package:flutter/material.dart'; -import 'package:sqflite/sqflite.dart'; - -void main() => runApp(const SqfliteExampleApp()); - -/// Laps saved to a SQLite database on the watch. -class SqfliteExampleApp extends StatelessWidget { - /// The example app. - const SqfliteExampleApp({super.key}); - - @override - Widget build(BuildContext context) => MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData.dark(useMaterial3: true), - home: const LapsPage(), - ); -} - -/// Add, list and clear laps. -class LapsPage extends StatefulWidget { - /// The one page. - const LapsPage({super.key}); - - @override - State createState() => _LapsPageState(); -} - -class _LapsPageState extends State { - Database? _db; - List> _laps = const >[]; - String? _error; - - @override - void initState() { - super.initState(); - _open(); - } - - @override - void dispose() { - _db?.close(); - super.dispose(); - } - - Future _open() async { - try { - _db = await openDatabase( - 'laps.db', - version: 1, - onCreate: (Database db, int version) => db.execute( - 'CREATE TABLE lap (id INTEGER PRIMARY KEY, at INTEGER NOT NULL)'), - ); - await _reload(); - } on DatabaseException catch (e) { - setState(() => _error = '$e'); - } - } - - Future _reload() async { - final List> laps = - await _db!.query('lap', orderBy: 'id DESC'); - setState(() => _laps = laps); - } - - Future _add() async { - await _db!.insert('lap', { - 'at': DateTime.now().millisecondsSinceEpoch, - }); - await _reload(); - } - - Future _clear() async { - await _db!.delete('lap'); - await _reload(); - } - - String _time(Object? millis) { - final DateTime t = DateTime.fromMillisecondsSinceEpoch(millis! as int); - String two(int v) => v.toString().padLeft(2, '0'); - return '${two(t.hour)}:${two(t.minute)}:${two(t.second)}'; - } - - @override - Widget build(BuildContext context) => Scaffold( - body: ListView( - padding: const EdgeInsets.fromLTRB(12, 28, 12, 24), - children: [ - Text('${_laps.length} laps', - style: - const TextStyle(fontSize: 24, fontWeight: FontWeight.w600)), - if (_error != null) - Text(_error!, style: const TextStyle(color: Colors.orangeAccent)), - FilledButton( - onPressed: _db == null ? null : _add, - child: const Text('Add lap'), - ), - for (final Map lap in _laps.take(5)) - Text('#${lap['id']} ${_time(lap['at'])}'), - if (_laps.isNotEmpty) - TextButton(onPressed: _clear, child: const Text('Clear')), - ], - ), - ); +Future main() async { + supportsCompatMode = true; + mainExampleApp(); } diff --git a/packages/sqflite_watchos/example/pubspec.yaml b/packages/sqflite_watchos/example/pubspec.yaml index ef6fa26..f36b313 100644 --- a/packages/sqflite_watchos/example/pubspec.yaml +++ b/packages/sqflite_watchos/example/pubspec.yaml @@ -1,28 +1,44 @@ name: sqflite_example -description: "watchOS example for sqflite_watchos." -publish_to: 'none' -version: 1.0.0+1 +description: Demonstrates how to use the sqflite plugin and perform some tests. +publish_to: none +version: "0.4.0" environment: sdk: ">=3.1.0 <4.0.0" +# sqflite 2.4.5's own example. Each version is the one it ran with here, as +# a caret, since this repository's CI refuses an unconstrained example +# dependency. Two watch deviations: sqflite_example_common is not on +# pub.dev, so it is pinned to the tekartik/sqflite commit of sqflite 2.4.5, +# and sqflite_watchos is added. dependencies: + path: ^1.9.1 + collection: ^1.19.1 flutter: sdk: flutter - sqflite: ^2.4.4 + sqflite: ^2.4.5 + sqflite_common: ^2.5.14 + synchronized: ^3.5.0 + matcher: ^0.12.20 + meta: ^1.19.0 + sqflite_example_common: + git: + url: https://github.com/tekartik/sqflite + path: packages_flutter/sqflite_example_common + ref: 21d2b8d0ab53159a73710db90857d928fad09c84 sqflite_watchos: path: ../ - path: ^1.9.0 dev_dependencies: + flutter_lints: ^6.0.0 flutter_test: sdk: flutter integration_test: sdk: flutter - flutter_lints: ^4.0.0 - # The official integration test imports these two. - material_ui: ^1.5.0 - test: ^1.25.0 + test: ^1.31.1 + process_run: ^1.4.1 + pub_semver: ^2.2.1 + material_ui: ^1.9.0 flutter: uses-material-design: true diff --git a/tool/README.md b/tool/README.md index e9a2c8a..6d4412e 100644 --- a/tool/README.md +++ b/tool/README.md @@ -104,7 +104,8 @@ that no longer matches. It moved here from or `dev_dependencies`. An example built against whatever is newest that day can break with no change in this repository. Use a caret constraint on the version the example was last run with; `path:` and `sdk:` entries and -`dependency_overrides` are not checked. +`dependency_overrides` are not checked. A `git:` dependency must set `ref` +to a full commit hash: a branch, a tag or no `ref` can move the same way. ## Publish dry-run diff --git a/tool/lib/src/example_constraints.dart b/tool/lib/src/example_constraints.dart index ed0aeda..093d894 100644 --- a/tool/lib/src/example_constraints.dart +++ b/tool/lib/src/example_constraints.dart @@ -9,6 +9,11 @@ /// flutter_secure_storage example resolved an upstream major its official /// test no longer compiles against). No example pubspec may name `any`, or /// leave a constraint empty, under `dependencies` or `dev_dependencies`. +/// +/// A git dependency moves the same way unless its `ref` names one commit: +/// a branch, a tag or no `ref` at all can resolve to new code tomorrow. So a +/// git dependency must pin a full commit hash (sqflite's example takes +/// `sqflite_example_common`, which is not on pub.dev, from its repository). library; import 'dart:io'; @@ -16,6 +21,9 @@ import 'dart:io'; import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart'; +/// A full git commit hash, as `ref` must name it. +final RegExp _commitHash = RegExp(r'^[0-9a-f]{40}$'); + /// The dependencies of the pubspec [contents] that name `any` or no /// constraint, as `section: name`. List unconstrainedDependencies(String contents) { @@ -35,6 +43,31 @@ List unconstrainedDependencies(String contents) { return found; } +/// The git dependencies of the pubspec [contents] whose `ref` is not a full +/// commit hash, as `section: name`. A `git:` given as a plain URL has no +/// `ref`, so it is one of them. +List unpinnedGitDependencies(String contents) { + final Object? pubspec = loadYaml(contents); + final List found = []; + for (final String section in ['dependencies', 'dev_dependencies']) { + final Object? dependencies = pubspec is YamlMap ? pubspec[section] : null; + if (dependencies is! YamlMap) { + continue; + } + dependencies.forEach((Object? name, Object? value) { + if (value is! YamlMap || !value.containsKey('git')) { + return; + } + final Object? git = value['git']; + final Object? ref = git is YamlMap ? git['ref'] : null; + if (ref is! String || !_commitHash.hasMatch(ref)) { + found.add('$section: $name'); + } + }); + } + return found; +} + /// Checks `packages/*/example/pubspec.yaml` of the repository at [root], /// writes the report to [out] and returns the exit code. int runExampleConstraintCheck(String root, StringSink out) { @@ -55,21 +88,29 @@ int runExampleConstraintCheck(String root, StringSink out) { continue; } examples++; - final List found = unconstrainedDependencies( - pubspec.readAsStringSync(), - ); - if (found.isEmpty) { + final String contents = pubspec.readAsStringSync(); + final List unconstrained = unconstrainedDependencies(contents); + final List unpinned = unpinnedGitDependencies(contents); + if (unconstrained.isEmpty && unpinned.isEmpty) { out.writeln('ok $package/example'); continue; } failures++; out.writeln('FAIL $package/example'); - for (final String dependency in found) { + for (final String dependency in unconstrained) { out.writeln(' $dependency has no constraint; use ^x.y.z.'); } + for (final String dependency in unpinned) { + out.writeln( + ' $dependency is a git dependency without a pinned commit; ' + 'set its ref to a full commit hash.', + ); + } } if (failures > 0) { - out.writeln('$failures of $examples examples depend on "any".'); + out.writeln( + '$failures of $examples examples have a dependency that can move.', + ); return 1; } out.writeln('Example constraints: all $examples examples pass.'); diff --git a/tool/test/example_constraints_test.dart b/tool/test/example_constraints_test.dart index 5c1aa70..631acb7 100644 --- a/tool/test/example_constraints_test.dart +++ b/tool/test/example_constraints_test.dart @@ -48,4 +48,45 @@ void main() { ['dependencies: a', 'dev_dependencies: b'], ); }); + + test('a fixture example with an unpinned git dependency fails', () { + for (final String name in ['git_unpinned', 'git_branch_ref']) { + expect( + fixtures.singleWhere((Fixture f) => f.name == name).expectedExitCode, + 1, + reason: name, + ); + } + }); + + test('a git dependency must name a full commit hash as its ref', () { + const String hash = '0123456789abcdef0123456789abcdef01234567'; + expect( + unpinnedGitDependencies( + 'dependencies:\n' + ' url_only:\n git: https://example.com/a.git\n' + ' no_ref:\n git:\n url: https://example.com/b.git\n' + ' branch:\n git:\n url: https://example.com/c.git\n' + ' ref: main\n' + ' short_hash:\n git:\n url: https://example.com/d.git\n' + ' ref: 0123456\n' + ' full_hash:\n git:\n url: https://example.com/e.git\n' + ' ref: $hash\n' + ' hosted: ^1.0.0\n' + ' local:\n path: ../\n' + 'dev_dependencies:\n' + ' tag:\n git:\n url: https://example.com/f.git\n' + ' ref: v1.0.0\n' + 'dependency_overrides:\n' + ' overridden:\n git: https://example.com/g.git\n', + ), + [ + 'dependencies: url_only', + 'dependencies: no_ref', + 'dependencies: branch', + 'dependencies: short_hash', + 'dev_dependencies: tag', + ], + ); + }); } diff --git a/tool/test/fixtures/example_constraints/git_branch_ref/expect b/tool/test/fixtures/example_constraints/git_branch_ref/expect new file mode 100644 index 0000000..d5e4105 --- /dev/null +++ b/tool/test/fixtures/example_constraints/git_branch_ref/expect @@ -0,0 +1,3 @@ +1 +FAIL up_watchos/example +dependencies: up_common is a git dependency without a pinned commit diff --git a/tool/test/fixtures/example_constraints/git_branch_ref/repo/packages/up_watchos/example/pubspec.yaml b/tool/test/fixtures/example_constraints/git_branch_ref/repo/packages/up_watchos/example/pubspec.yaml new file mode 100644 index 0000000..3b2c945 --- /dev/null +++ b/tool/test/fixtures/example_constraints/git_branch_ref/repo/packages/up_watchos/example/pubspec.yaml @@ -0,0 +1,16 @@ +name: example +publish_to: none +dependencies: + flutter: + sdk: flutter + up: ^2.1.0 + up_common: + git: + url: https://example.com/up.git + path: up_common + ref: main + up_watchos: + path: ../ +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/tool/test/fixtures/example_constraints/git_pinned/expect b/tool/test/fixtures/example_constraints/git_pinned/expect new file mode 100644 index 0000000..e8b4d99 --- /dev/null +++ b/tool/test/fixtures/example_constraints/git_pinned/expect @@ -0,0 +1,3 @@ +0 +ok up_watchos/example +all 1 examples pass diff --git a/tool/test/fixtures/example_constraints/git_pinned/repo/packages/up_watchos/example/pubspec.yaml b/tool/test/fixtures/example_constraints/git_pinned/repo/packages/up_watchos/example/pubspec.yaml new file mode 100644 index 0000000..9104fa7 --- /dev/null +++ b/tool/test/fixtures/example_constraints/git_pinned/repo/packages/up_watchos/example/pubspec.yaml @@ -0,0 +1,16 @@ +name: example +publish_to: none +dependencies: + flutter: + sdk: flutter + up: ^2.1.0 + up_common: + git: + url: https://example.com/up.git + path: up_common + ref: 0123456789abcdef0123456789abcdef01234567 + up_watchos: + path: ../ +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/tool/test/fixtures/example_constraints/git_unpinned/expect b/tool/test/fixtures/example_constraints/git_unpinned/expect new file mode 100644 index 0000000..c878d6b --- /dev/null +++ b/tool/test/fixtures/example_constraints/git_unpinned/expect @@ -0,0 +1,4 @@ +1 +FAIL up_watchos/example +dependencies: up_common is a git dependency without a pinned commit +dev_dependencies: up_tools is a git dependency without a pinned commit diff --git a/tool/test/fixtures/example_constraints/git_unpinned/repo/packages/up_watchos/example/pubspec.yaml b/tool/test/fixtures/example_constraints/git_unpinned/repo/packages/up_watchos/example/pubspec.yaml new file mode 100644 index 0000000..048eae2 --- /dev/null +++ b/tool/test/fixtures/example_constraints/git_unpinned/repo/packages/up_watchos/example/pubspec.yaml @@ -0,0 +1,17 @@ +name: example +publish_to: none +dependencies: + flutter: + sdk: flutter + up: ^2.1.0 + up_common: + git: + url: https://example.com/up.git + path: up_common + up_watchos: + path: ../ +dev_dependencies: + flutter_test: + sdk: flutter + up_tools: + git: https://example.com/up_tools.git