Test that the locked version of dependencies of sdk packages equal the lower bound (#183395)

Part of https://flutter.dev/go/unpin-flutter-sdk-dependencies
Preparing for https://github.com/flutter/flutter/issues/158050

This test will trivially succeed as long as packages are pinned.
But when we start unpinning it will be desirable to enforce that we
always run tests against the lower bounds of our dependencies.
diff --git a/packages/flutter_tools/test/integration.shard/dependencies_test.dart b/packages/flutter_tools/test/integration.shard/dependencies_test.dart
new file mode 100644
index 0000000..4286f5d
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/dependencies_test.dart
@@ -0,0 +1,203 @@
+// Copyright 2014 The Flutter Authors. 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:convert';
+import 'dart:io';
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+/// These are the sdk packages living inside the Flutter SDK that apps can
+/// depend on.
+const List<String> sdkPackages = <String>['flutter', 'flutter_test', 'flutter_localizations'];
+
+/// List of allowed external packages that Flutter framework packages can depend on.
+/// Subject to review and approval when adding new packages.
+///
+/// External dependencies from Flutter framework packages cause a number of
+/// issues including:
+///  - Increased risk of dependency conflicts for Flutter application developers.
+///  - Increased maintenance burden to keep dependencies up to date and secure.
+///  - Increased risk of breaking changes from upstream packages.
+///  - Increased attack surface for supply chain attacks.
+///
+///  Instead of adding new external dependencies, consider if the functionality
+///  can be implemented directly in the Flutter framework.
+///
+///  For any new external dependency, ensure that it is published by a trusted
+///  source, has a strong maintenance history, and has a stable API.
+final List<String> allowedExternalPackages = <String>[
+  'async',
+  'boolean_selector',
+  'characters',
+  'clock',
+  'collection',
+  'fake_async',
+  'intl',
+  'leak_tracker',
+  'leak_tracker_flutter_testing',
+  'leak_tracker_testing',
+  'matcher',
+  'material_color_utilities',
+  'meta',
+  'path',
+  'sky_engine',
+  'source_span',
+  'stack_trace',
+  'stream_channel',
+  'string_scanner',
+  'term_glyph',
+  'test_api',
+  'vector_math',
+  'vm_service',
+];
+
+Future<void> main() async {
+  test('only allowed dependencies are used in flutter framework packages', () {
+    // This test will explore the dependency graph of the current resolution.
+    // Newer versions of packages may add new dependencies, this test cannot.
+    // protect against that.
+    final String flutterRoot = Platform.environment['FLUTTER_ROOT']!;
+
+    // We assume that `pub get` has already been run and the
+    // .dart_tool/package_graph.json file is available.
+    final packageGraphFile = File.fromUri(
+      Directory(flutterRoot).uri.resolve('.dart_tool/package_graph.json'),
+    );
+    final packageGraph = jsonDecode(packageGraphFile.readAsStringSync()) as Map<String, Object?>;
+
+    final allowedPackages = <String>{
+      // Allow depending on other Flutter framework sdk packages.
+      ...sdkPackages,
+      // Allow depending on allowed external packages.
+      ...allowedExternalPackages,
+    };
+    final packages = packageGraph['packages']! as List<Object?>;
+    final packagesByName = <String, Map<String, Object?>>{
+      for (final package in packages)
+        (package! as Map<String, Object?>)['name']! as String: package as Map<String, Object?>,
+    };
+
+    // Do a transitive parse of the package graph rooted in `sdkPackages` to find any
+    // disallowed dependencies.
+    final toVisit = <String?>[...sdkPackages];
+    final visited = <String>{};
+    final stack = <String>[];
+    while (toVisit.isNotEmpty) {
+      final String? currentName = toVisit.removeLast();
+      if (currentName == null) {
+        stack.removeLast();
+        continue;
+      }
+      if (!visited.add(currentName)) {
+        continue;
+      }
+      if (!allowedPackages.contains(currentName)) {
+        stdout.writeln('Package "$currentName" is not in the allowed dependencies list.');
+        stdout.writeln('Dependency chain: ${[...stack, currentName].join(' -> ')}');
+        fail('''
+Package "$currentName" is not in the allowed dependencies list.
+
+Either remove the dependency or add it to the allowed dependencies list:
+  packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart
+''');
+      }
+      final Map<String, Object?> currentPackage = packagesByName[currentName]!;
+      final dependencies = currentPackage['dependencies']! as List<Object?>;
+      toVisit.add(null); // Marker for when we are done with this package.
+      toVisit.addAll(dependencies.cast<String>());
+      stack.add(currentName);
+    }
+    final List<String> unusedAllowedPackages = allowedExternalPackages.where((String package) {
+      return !visited.contains(package);
+    }).toList();
+    if (unusedAllowedPackages.isNotEmpty) {
+      fail('''
+Some allowed packages are not used from any sdk framework packages.
+Either use them or remove them from the allowlist:
+${unusedAllowedPackages.map((String package) => ' - $package').join('\n')}
+
+See: packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart
+''');
+    }
+  });
+
+  test('All dependencies are locked to their lower bound', () {
+    // This test will ensure that the lower bound of the version constraints from
+    // the sdk packages, are all equal to the version of the package in the lockfile.
+    //
+    // This is to ensure that we test against the lower bounds of our dependencies.
+    // So we don't accidentally rely on features added after the lower bound version.
+    final String flutterRoot = Platform.environment['FLUTTER_ROOT']!;
+    final flutterRootDirectory = Directory(flutterRoot);
+    // These are the sdk packages that Flutter apps can depend on.
+    final lockfile = File.fromUri(flutterRootDirectory.uri.resolve('pubspec.lock'));
+    final lockfileJson = loadYaml(lockfile.readAsStringSync()) as YamlMap;
+    final lockedDependencies = lockfileJson['packages']! as YamlMap;
+
+    for (final String sdkPackage in sdkPackages) {
+      final pubspecFile = File.fromUri(
+        flutterRootDirectory.uri.resolve('packages/$sdkPackage/pubspec.yaml'),
+      );
+      final pubspec = loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
+      final dependencies = pubspec['dependencies']! as YamlMap;
+      for (final MapEntry<Object?, Object?> dependency in dependencies.entries) {
+        final dependencyName = dependency.key! as String;
+        final Object? descriptor = dependency.value;
+        final VersionConstraint constraint;
+        if (descriptor is String) {
+          constraint = VersionConstraint.parse(descriptor);
+        } else if (descriptor is YamlMap) {
+          if (descriptor['sdk'] == 'flutter') {
+            // These are allowed.
+            continue;
+          }
+          if (descriptor['hosted'] == null) {
+            fail('''
+Dependency "$dependencyName" in "${pubspecFile.path}" is not a hosted or sdkpackage.
+''');
+          }
+          final versionString = descriptor['version'] as String?;
+          if (versionString == null) {
+            fail('''
+Dependency "$dependencyName" in "${pubspecFile.path}" has no version constraint.
+''');
+          }
+          constraint = VersionConstraint.parse(versionString);
+        } else {
+          fail('''
+Dependency "$dependencyName" in "${pubspecFile.path}" is not a hosted or sdk package.
+''');
+        }
+        if (constraint is! VersionRange) {
+          fail('''
+Dependency "$dependencyName" in "${pubspecFile.path}" is not a version range.
+''');
+        }
+        if (constraint.min == null) {
+          fail('''
+Dependency "$dependencyName" in "${pubspecFile.path}" has no lower bound.
+''');
+        }
+        if (constraint.min != constraint.max) {
+          fail('''
+Dependency "$dependencyName" in "${pubspecFile.path}" is not locked at its lower bound.
+''');
+        }
+
+        final currentVersion = Version.parse(
+          (lockedDependencies[dependencyName]! as YamlMap)['version']! as String,
+        );
+
+        if (currentVersion != constraint.min) {
+          fail('''
+Dependency "$dependencyName" is not locked at its lower bound in "${pubspecFile.path}".
+Either upgrade the constraint to $currentVersion in pubspec.yaml or downgrade the version to ${constraint.min} in pubspec.lock.
+''');
+        }
+      }
+    }
+  });
+}
diff --git a/packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart b/packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart
deleted file mode 100644
index bc06e72..0000000
--- a/packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright 2014 The Flutter Authors. 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:convert';
-import 'dart:io';
-import 'package:test/test.dart';
-
-/// List of allowed external packages that Flutter framework packages can depend on.
-/// Subject to review and approval when adding new packages.
-///
-/// External dependencies from Flutter framework packages cause a number of
-/// issues including:
-///  - Increased risk of dependency conflicts for Flutter application developers.
-///  - Increased maintenance burden to keep dependencies up to date and secure.
-///  - Increased risk of breaking changes from upstream packages.
-///  - Increased attack surface for supply chain attacks.
-///
-///  Instead of adding new external dependencies, consider if the functionality
-///  can be implemented directly in the Flutter framework.
-///
-///  For any new external dependency, ensure that it is published by a trusted
-///  source, has a strong maintenance history, and has a stable API.
-final List<String> allowedExternalPackages = <String>[
-  'async',
-  'boolean_selector',
-  'characters',
-  'clock',
-  'collection',
-  'fake_async',
-  'intl',
-  'leak_tracker',
-  'leak_tracker_flutter_testing',
-  'leak_tracker_testing',
-  'matcher',
-  'material_color_utilities',
-  'meta',
-  'path',
-  'sky_engine',
-  'source_span',
-  'stack_trace',
-  'stream_channel',
-  'string_scanner',
-  'term_glyph',
-  'test_api',
-  'vector_math',
-  'vm_service',
-];
-
-Future<void> main() async {
-  test('only allowed dependencies are used in flutter framework packages', () {
-    // This test will explore the dependency graph of the current resolution.
-    // Newer versions of packages may add new dependencies, this test cannot.
-    // protect against that.
-    final String flutterRoot = Platform.environment['FLUTTER_ROOT']!;
-
-    // We assume that `pub get` has already been run and the
-    // .dart_tool/package_graph.json file is available.
-    final packageGraphFile = File.fromUri(
-      Directory(flutterRoot).uri.resolve('.dart_tool/package_graph.json'),
-    );
-    final packageGraph = jsonDecode(packageGraphFile.readAsStringSync()) as Map<String, Object?>;
-
-    // These are the sdk packages that Flutter apps can depend on.
-    final roots = <String>['flutter', 'flutter_test', 'flutter_localizations'];
-    final allowedPackages = <String>{
-      // Allow depending on other Flutter framework sdk packages.
-      ...roots,
-      // Allow depending on allowed external packages.
-      ...allowedExternalPackages,
-    };
-    final packages = packageGraph['packages']! as List<Object?>;
-    final packagesByName = <String, Map<String, Object?>>{
-      for (final package in packages)
-        (package! as Map<String, Object?>)['name']! as String: package as Map<String, Object?>,
-    };
-
-    // Do a transitive parse of the package graph rooted in `roots` to find any
-    // disallowed dependencies.
-    final toVisit = <String?>[...roots];
-    final visited = <String>{};
-    final stack = <String>[];
-    while (toVisit.isNotEmpty) {
-      final String? currentName = toVisit.removeLast();
-      if (currentName == null) {
-        stack.removeLast();
-        continue;
-      }
-      if (!visited.add(currentName)) {
-        continue;
-      }
-      if (!allowedPackages.contains(currentName)) {
-        stdout.writeln('Package "$currentName" is not in the allowed dependencies list.');
-        stdout.writeln('Dependency chain: ${[...stack, currentName].join(' -> ')}');
-        fail('''
-Package "$currentName" is not in the allowed dependencies list.
-
-Either remove the dependency or add it to the allowed dependencies list:
-  packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart
-''');
-      }
-      final Map<String, Object?> currentPackage = packagesByName[currentName]!;
-      final dependencies = currentPackage['dependencies']! as List<Object?>;
-      toVisit.add(null); // Marker for when we are done with this package.
-      toVisit.addAll(dependencies.cast<String>());
-      stack.add(currentName);
-    }
-    final List<String> unusedAllowedPackages = allowedExternalPackages.where((String package) {
-      return !visited.contains(package);
-    }).toList();
-    if (unusedAllowedPackages.isNotEmpty) {
-      fail('''
-Some allowed packages are not used from any sdk framework packages.
-Either use them or remove them from the allowlist:
-${unusedAllowedPackages.map((String package) => ' - $package').join('\n')}
-
-See: packages/flutter_tools/test/integration.shard/only_allowed_dependencies_test.dart
-''');
-    }
-  });
-}