[flutter_releases] Flutter beta 2.8.0-3.2.pre Framework Cherrypicks (#93841)

* [flutter_tools] Catch lack of flutter tools source missing (#93168)

* 'Update Engine revision to bcc2b7f12cada3d1359e353c416568b4c3f6df69 for beta release 2.8.0-3.2.pre'

* Replace text directionality control characters with escape sequences in the semantics_tester (#93034)

Co-authored-by: Christopher Fujino <christopherfujino@gmail.com>
Co-authored-by: Jason Simmons <jason-simmons@users.noreply.github.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index d2dd5b3..c4e64cc 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-09f1520e8b9585d133faf1eccced9357670c6d11
+bcc2b7f12cada3d1359e353c416568b4c3f6df69
diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart
index ed2e8eb..03cc46d 100644
--- a/packages/flutter/test/widgets/semantics_tester.dart
+++ b/packages/flutter/test/widgets/semantics_tester.dart
@@ -635,12 +635,9 @@
     if (nodeData.actions != 0)
       buf.writeln('  actions: ${_actionsToSemanticsActionExpression(nodeData.actions)},');
     if (node.label != null && node.label.isNotEmpty) {
-      final String escapedLabel = node.label.replaceAll('\n', r'\n');
-      if (escapedLabel != node.label) {
-        buf.writeln("  label: r'$escapedLabel',");
-      } else {
-        buf.writeln("  label: '$escapedLabel',");
-      }
+      // Escape newlines and text directionality control characters.
+      final String escapedLabel = node.label.replaceAll('\n', r'\n').replaceAll('\u202a', r'\u202a').replaceAll('\u202c', r'\u202c');
+      buf.writeln("  label: '$escapedLabel',");
     }
     if (node.value != null && node.value.isNotEmpty)
       buf.writeln("  value: '${node.value}',");
diff --git a/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart b/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart
index 0336aaf..eb3c58f 100644
--- a/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart
+++ b/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart
@@ -132,7 +132,7 @@
                                   tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
                                   flags: <SemanticsFlag>[SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected],
                                   actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.decrease],
-                                  label: '‪Interactive text‬',
+                                  label: '\u202aInteractive text\u202c',
                                   value: 'test-value',
                                   increasedValue: 'test-increasedValue',
                                   decreasedValue: 'test-decreasedValue',
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index dffc009..de2ce3c 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -54,6 +54,7 @@
 // Files in `isolated` are intentionally excluded from google3 tooling.
 import 'src/isolated/mustache_template.dart';
 import 'src/isolated/resident_web_runner.dart';
+import 'src/pre_run_validator.dart';
 import 'src/resident_runner.dart';
 import 'src/runner/flutter_command.dart';
 import 'src/web/web_runner.dart';
@@ -126,6 +127,7 @@
           windows: globals.platform.isWindows,
         );
       },
+      PreRunValidator: () => PreRunValidator(fileSystem: globals.fs),
     },
   );
 }
diff --git a/packages/flutter_tools/lib/src/globals_null_migrated.dart b/packages/flutter_tools/lib/src/globals_null_migrated.dart
index e59eea7..29b2423 100644
--- a/packages/flutter_tools/lib/src/globals_null_migrated.dart
+++ b/packages/flutter_tools/lib/src/globals_null_migrated.dart
@@ -39,6 +39,7 @@
 import 'macos/xcdevice.dart';
 import 'macos/xcode.dart';
 import 'persistent_tool_state.dart';
+import 'pre_run_validator.dart';
 import 'project.dart';
 import 'reporting/crash_reporting.dart';
 import 'reporting/reporting.dart';
@@ -233,3 +234,5 @@
 }
 
 CustomDevicesConfig get customDevicesConfig => context.get<CustomDevicesConfig>()!;
+
+PreRunValidator get preRunValidator => context.get<PreRunValidator>() ?? const NoOpPreRunValidator();
diff --git a/packages/flutter_tools/lib/src/pre_run_validator.dart b/packages/flutter_tools/lib/src/pre_run_validator.dart
new file mode 100644
index 0000000..e08cf1c
--- /dev/null
+++ b/packages/flutter_tools/lib/src/pre_run_validator.dart
@@ -0,0 +1,51 @@
+// 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 'base/common.dart';
+import 'base/file_system.dart';
+import 'cache.dart';
+
+/// A validator that runs before the tool runs any command.
+abstract class PreRunValidator {
+  factory PreRunValidator({
+    required FileSystem fileSystem,
+  }) => _DefaultPreRunValidator(fileSystem: fileSystem);
+
+  void validate();
+}
+
+class _DefaultPreRunValidator implements PreRunValidator {
+  _DefaultPreRunValidator({
+    required this.fileSystem,
+  });
+
+  final FileSystem fileSystem;
+
+  late final Directory _toolsDir = fileSystem.directory(
+      fileSystem.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools'),
+  );
+
+  @override
+  void validate() {
+    // If a user downloads the Flutter SDK via a pre-built archive and there is
+    // an error during extraction, the user could have a valid Dart snapshot of
+    // the tool but not the source directory. We still need the source, so
+    // validate the source directory exists and toolExit if not.
+    if (!_toolsDir.existsSync()) {
+      throwToolExit(
+        'Flutter SDK installation appears corrupted: expected to find the '
+        'directory ${_toolsDir.path} but it does not exist! Please go to '
+        'https://flutter.dev/setup for instructions on how to re-install '
+        'Flutter.',
+      );
+    }
+  }
+}
+
+class NoOpPreRunValidator implements PreRunValidator {
+  const NoOpPreRunValidator();
+
+  @override
+  void validate() {}
+}
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 4b3c8be..3398299 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -1234,6 +1234,7 @@
   /// rather than calling [runCommand] directly.
   @mustCallSuper
   Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
+    globals.preRunValidator.validate();
     // Populate the cache. We call this before pub get below so that the
     // sky_engine package is available in the flutter cache for pub to find.
     if (shouldUpdateCache) {
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/shell_completion_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/shell_completion_test.dart
index d2fda26..914e8b1 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/shell_completion_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/shell_completion_test.dart
@@ -31,6 +31,8 @@
       expect(fakeStdio.writtenToStdout.length, equals(1));
       expect(fakeStdio.writtenToStdout.first, contains('__flutter_completion'));
     }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem.test(),
+      ProcessManager: () => FakeProcessManager.any(),
       Stdio: () => fakeStdio,
     });
 
@@ -40,6 +42,8 @@
       expect(fakeStdio.writtenToStdout.length, equals(1));
       expect(fakeStdio.writtenToStdout.first, contains('__flutter_completion'));
     }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem.test(),
+      ProcessManager: () => FakeProcessManager.any(),
       Stdio: () => fakeStdio,
     });
 
diff --git a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart
index 95c5b0b..e0bafd7 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart
@@ -449,6 +449,7 @@
 
     testUsingContext('test without bot', () async {
       Cache.flutterRoot = '';
+      globals.fs.directory('/packages/flutter_tools').createSync(recursive: true);
       globals.fs.file('pubspec.yaml').createSync();
       processManager.addCommand(
         const FakeCommand(command: <String>['/bin/cache/dart-sdk/bin/dart', '__deprecated_pub', 'run', 'test']),
diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
index e770c18..77a09dd 100644
--- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
+++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
@@ -18,7 +18,8 @@
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/dart/pub.dart';
-import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
+import 'package:flutter_tools/src/globals.dart' as globals;
+import 'package:flutter_tools/src/pre_run_validator.dart';
 import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:test/fake.dart';
@@ -34,6 +35,13 @@
     TestUsage usage;
     FakeClock clock;
     FakeProcessInfo processInfo;
+    MemoryFileSystem fileSystem;
+    FakeProcessManager processManager;
+    PreRunValidator preRunValidator;
+
+    setUpAll(() {
+      Cache.flutterRoot = '/path/to/sdk/flutter';
+    });
 
     setUp(() {
       Cache.disableLocking();
@@ -42,6 +50,9 @@
       clock = FakeClock();
       processInfo = FakeProcessInfo();
       processInfo.maxRss = 10;
+      fileSystem = MemoryFileSystem.test();
+      processManager = FakeProcessManager.empty();
+      preRunValidator = PreRunValidator(fileSystem: fileSystem);
     });
 
     tearDown(() {
@@ -63,6 +74,8 @@
       expect(flutterCommand.hidden, isFalse);
     },
     overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
       Cache: () => cache,
     });
 
@@ -79,9 +92,25 @@
       );
     },
     overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
       Cache: () => cache,
     });
 
+    testUsingContext("throws toolExit if flutter_tools source dir doesn't exist", () async {
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
+      await expectToolExitLater(
+        flutterCommand.run(),
+        contains('Flutter SDK installation appears corrupted'),
+      );
+    },
+    overrides: <Type, Generator>{
+      Cache: () => cache,
+      FileSystem: () => fileSystem,
+      PreRunValidator: () => preRunValidator,
+      ProcessManager: () => processManager,
+    });
+
     testUsingContext('deprecated command should warn', () async {
       final FakeDeprecatedCommand flutterCommand = FakeDeprecatedCommand();
       final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
@@ -95,6 +124,9 @@
             'of Flutter.'));
       expect(flutterCommand.deprecated, isTrue);
       expect(flutterCommand.hidden, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('uses the error handling file system', () async {
@@ -105,6 +137,9 @@
         }
       );
       await flutterCommand.run();
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('finds the target file with default values', () async {
@@ -115,8 +150,8 @@
 
       expect(fakeTargetCommand.cachedTargetFile, 'lib/main.dart');
     }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem.test(),
-      ProcessManager: () => FakeProcessManager.any(),
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('finds the target file with specified value', () async {
@@ -127,8 +162,8 @@
 
       expect(fakeTargetCommand.cachedTargetFile, 'lib/foo.dart');
     }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem.test(),
-      ProcessManager: () => FakeProcessManager.any(),
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('throws tool exit if specified file does not exist', () async {
@@ -137,13 +172,15 @@
 
       expect(() async => runner.run(<String>['test', '-t', 'lib/foo.dart']), throwsToolExit());
     }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem.test(),
-      ProcessManager: () => FakeProcessManager.any(),
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     void testUsingCommandContext(String testName, dynamic Function() testBody) {
       testUsingContext(testName, testBody, overrides: <Type, Generator>{
+        FileSystem: () => fileSystem,
         ProcessInfo: () => processInfo,
+        ProcessManager: () => processManager,
         SystemClock: () => clock,
         Usage: () => usage,
       });
@@ -245,6 +282,9 @@
         'http://127.0.0.1:9105',
       ]);
       expect(command.devToolsServerAddress.toString(), equals('http://127.0.0.1:9105'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('devToolsServerAddress returns null for bad input', () async {
@@ -277,6 +317,9 @@
         '127.0.0.1:9101',
       ]);
       expect(command.devToolsServerAddress, isNull);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     group('signals tests', () {
@@ -328,6 +371,8 @@
           ),
         ]);
       }, overrides: <Type, Generator>{
+        FileSystem: () => fileSystem,
+        ProcessManager: () => processManager,
         ProcessInfo: () => processInfo,
         Signals: () => FakeSignals(
           subForSigTerm: signalUnderTest,
@@ -363,6 +408,8 @@
         signalController.add(mockSignal);
         await completer.future;
       }, overrides: <Type, Generator>{
+        FileSystem: () => fileSystem,
+        ProcessManager: () => processManager,
         ProcessInfo: () => processInfo,
         Signals: () => FakeSignals(
               subForSigTerm: signalUnderTest,
@@ -499,26 +546,35 @@
     }, overrides: <Type, Generator>{
       Pub: () => FakePub(),
       Usage: () => usage,
-      FileSystem: () => MemoryFileSystem.test(),
-      ProcessManager: () => FakeProcessManager.any(),
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('use packagesPath to generate BuildInfo', () async {
       final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
       final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
       expect(buildInfo.packagesPath, 'foo');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('use fileSystemScheme to generate BuildInfo', () async {
       final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemScheme: 'foo');
       final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
       expect(buildInfo.fileSystemScheme, 'foo');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('use fileSystemRoots to generate BuildInfo', () async {
       final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemRoots: <String>['foo', 'bar']);
       final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
       expect(buildInfo.fileSystemRoots, <String>['foo', 'bar']);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('includes initializeFromDill in BuildInfo', () async {
@@ -527,6 +583,9 @@
       await runner.run(<String>['dummy', '--initialize-from-dill=/foo/bar.dill']);
       final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
       expect(buildInfo.initializeFromDill, '/foo/bar.dill');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('dds options', () async {
@@ -535,6 +594,9 @@
       await runner.run(<String>['test', '--dds-port=1']);
       expect(ddsCommand.enableDds, isTrue);
       expect(ddsCommand.ddsPort, 1);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('dds options --dds', () async {
@@ -542,6 +604,9 @@
       final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
       await runner.run(<String>['test', '--dds']);
       expect(ddsCommand.enableDds, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('dds options --no-dds', () async {
@@ -549,6 +614,9 @@
       final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
       await runner.run(<String>['test', '--no-dds']);
       expect(ddsCommand.enableDds, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('dds options --disable-dds', () async {
@@ -556,6 +624,9 @@
       final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
       await runner.run(<String>['test', '--disable-dds']);
       expect(ddsCommand.enableDds, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('dds options --no-disable-dds', () async {
@@ -563,6 +634,9 @@
       final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
       await runner.run(<String>['test', '--no-disable-dds']);
       expect(ddsCommand.enableDds, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
 
     testUsingContext('dds options --dds --disable-dds', () async {
@@ -570,6 +644,9 @@
       final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
       await runner.run(<String>['test', '--dds', '--disable-dds']);
       expect(() => ddsCommand.enableDds, throwsToolExit());
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fileSystem,
+      ProcessManager: () => processManager,
     });
   });
 }
diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart
index 6f0e311..2e00942 100644
--- a/packages/flutter_tools/test/src/common.dart
+++ b/packages/flutter_tools/test/src/common.dart
@@ -162,6 +162,7 @@
       addTearDown(() async {
         await globals.localFileSystem.dispose();
       });
+
       return body();
     },
     skip: skip,