Support SKP captures in flutter_tester (#25566)

diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc
index d0f70b6..acd3397 100644
--- a/shell/common/rasterizer.cc
+++ b/shell/common/rasterizer.cc
@@ -454,7 +454,6 @@
   // Deleting a surface also clears the GL context. Therefore, acquire the
   // frame after calling `BeginFrame` as this operation resets the GL context.
   auto frame = surface_->AcquireFrame(layer_tree.frame_size());
-
   if (frame == nullptr) {
     return RasterStatus::kFailed;
   }
diff --git a/shell/gpu/gpu_surface_software_delegate.h b/shell/gpu/gpu_surface_software_delegate.h
index 81cf30a..e46f4ec 100644
--- a/shell/gpu/gpu_surface_software_delegate.h
+++ b/shell/gpu/gpu_surface_software_delegate.h
@@ -21,7 +21,7 @@
 ///             rasterizer needs to allocate and present the software backing
 ///             store.
 ///
-/// @see        |IOSurfaceSoftware|, |AndroidSurfaceSoftware|,
+/// @see        |IOSSurfaceSoftware|, |AndroidSurfaceSoftware|,
 ///             |EmbedderSurfaceSoftware|.
 ///
 class GPUSurfaceSoftwareDelegate {
diff --git a/shell/testing/BUILD.gn b/shell/testing/BUILD.gn
index f82f481..678f620 100644
--- a/shell/testing/BUILD.gn
+++ b/shell/testing/BUILD.gn
@@ -28,6 +28,7 @@
     "//flutter/fml",
     "//flutter/lib/snapshot",
     "//flutter/shell/common",
+    "//flutter/shell/gpu:gpu_surface_software",
     "//flutter/third_party/tonic",
     "//third_party/dart/runtime:libdart_jit",
     "//third_party/dart/runtime/bin:dart_io_api",
diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc
index 6095ffe..cbdaabf 100644
--- a/shell/testing/tester_main.cc
+++ b/shell/testing/tester_main.cc
@@ -22,6 +22,7 @@
 #include "flutter/shell/common/shell.h"
 #include "flutter/shell/common/switches.h"
 #include "flutter/shell/common/thread_host.h"
+#include "flutter/shell/gpu/gpu_surface_software.h"
 #include "third_party/dart/runtime/include/bin/dart_io_api.h"
 #include "third_party/dart/runtime/include/dart_api.h"
 
@@ -31,6 +32,51 @@
 
 namespace flutter {
 
+class TesterPlatformView : public PlatformView,
+                           public GPUSurfaceSoftwareDelegate {
+ public:
+  TesterPlatformView(Delegate& delegate, TaskRunners task_runners)
+      : PlatformView(delegate, std::move(task_runners)) {}
+
+  // |PlatformView|
+  std::unique_ptr<Surface> CreateRenderingSurface() override {
+    auto surface = std::make_unique<GPUSurfaceSoftware>(
+        this, true /* render to surface */);
+    FML_DCHECK(surface->IsValid());
+    return surface;
+  }
+
+  // |GPUSurfaceSoftwareDelegate|
+  sk_sp<SkSurface> AcquireBackingStore(const SkISize& size) override {
+    if (sk_surface_ != nullptr &&
+        SkISize::Make(sk_surface_->width(), sk_surface_->height()) == size) {
+      // The old and new surface sizes are the same. Nothing to do here.
+      return sk_surface_;
+    }
+
+    SkImageInfo info =
+        SkImageInfo::MakeN32(size.fWidth, size.fHeight, kPremul_SkAlphaType,
+                             SkColorSpace::MakeSRGB());
+    sk_surface_ = SkSurface::MakeRaster(info, nullptr);
+
+    if (sk_surface_ == nullptr) {
+      FML_LOG(ERROR)
+          << "Could not create backing store for software rendering.";
+      return nullptr;
+    }
+
+    return sk_surface_;
+  }
+
+  // |GPUSurfaceSoftwareDelegate|
+  bool PresentBackingStore(sk_sp<SkSurface> backing_store) override {
+    return true;
+  }
+
+ private:
+  sk_sp<SkSurface> sk_surface_ = nullptr;
+};
+
 // Checks whether the engine's main Dart isolate has no pending work.  If so,
 // then exit the given message loop.
 class ScriptCompletionTaskObserver {
@@ -138,7 +184,8 @@
 
   Shell::CreateCallback<PlatformView> on_create_platform_view =
       [](Shell& shell) {
-        return std::make_unique<PlatformView>(shell, shell.GetTaskRunners());
+        return std::make_unique<TesterPlatformView>(shell,
+                                                    shell.GetTaskRunners());
       };
 
   Shell::CreateCallback<Rasterizer> on_create_rasterizer = [](Shell& shell) {
@@ -162,6 +209,8 @@
     return EXIT_FAILURE;
   }
 
+  shell->GetPlatformView()->NotifyCreated();
+
   // Initialize default testing locales. There is no platform to
   // pass locales on the tester, so to retain expected locale behavior,
   // we emulate it in here by passing in 'en_US' and 'zh_CN' as test locales.
diff --git a/testing/dart/observatory/README.md b/testing/dart/observatory/README.md
new file mode 100644
index 0000000..41b57d8
--- /dev/null
+++ b/testing/dart/observatory/README.md
@@ -0,0 +1,5 @@
+Tests in this folder need to be run with the observatory enabled, e.g. to make
+VM service method calls.
+
+The `run_tests.py` script disables the observatory for other tests in the
+parent directory.
\ No newline at end of file
diff --git a/testing/dart/observatory/skp_test.dart b/testing/dart/observatory/skp_test.dart
new file mode 100644
index 0000000..2b2e3e8
--- /dev/null
+++ b/testing/dart/observatory/skp_test.dart
@@ -0,0 +1,67 @@
+// Copyright 2013 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.
+
+// @dart=2.12
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:typed_data';
+import 'dart:ui';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart' as vms;
+import 'package:vm_service/vm_service_io.dart';
+
+void main() {
+  late vms.VmService vmService;
+
+  setUpAll(() async {
+    final developer.ServiceProtocolInfo info =
+        await developer.Service.getInfo();
+
+    if (info.serverUri == null) {
+      fail('This test must not be run with --disable-observatory.');
+    }
+
+    vmService = await vmServiceConnectUri(
+      'ws://localhost:${info.serverUri!.port}${info.serverUri!.path}ws',
+    );
+  });
+
+  tearDownAll(() async {
+    await vmService.dispose();
+  });
+
+  test('Capture an SKP ', () async {
+    final Completer<void> completer = Completer<void>();
+    window.onBeginFrame = (Duration timeStamp) {
+      final PictureRecorder recorder = PictureRecorder();
+      final Canvas canvas = Canvas(recorder);
+      canvas.drawRect(const Rect.fromLTRB(10, 10, 20, 20), Paint());
+      final Picture picture = recorder.endRecording();
+
+      final SceneBuilder builder = SceneBuilder();
+      builder.addPicture(Offset.zero, picture);
+      final Scene scene = builder.build();
+
+      window.render(scene);
+      scene.dispose();
+      // window.onBeginFrame = (Duration timeStamp) {
+        completer.complete();
+      // };
+      // window.scheduleFrame();
+    };
+    window.scheduleFrame();
+    await completer.future;
+
+    final vms.Response response = await vmService.callServiceExtension('_flutter.screenshotSkp');
+
+    final String base64data = response.json!['skp'] as String;
+    expect(base64data, isNotNull);
+    expect(base64data, isNotEmpty);
+    final Uint8List decoded = base64Decode(base64data);
+    expect(decoded.sublist(0, 8), 'skiapict'.codeUnits);
+  });
+}
diff --git a/testing/dart/pubspec.yaml b/testing/dart/pubspec.yaml
index a605e5b..9033468 100644
--- a/testing/dart/pubspec.yaml
+++ b/testing/dart/pubspec.yaml
@@ -4,9 +4,10 @@
   sdk: '>=2.8.0 <3.0.0'
 
 dependencies:
-  test: 1.3.0
-  path: 1.6.2
-  image: ^2.1.4
+  test: 1.16.8
+  path: 1.8.0
+  image: 3.0.2
+  vm_service: 6.2.0
 
 dependency_overrides:
  sky_engine:
diff --git a/testing/run_tests.py b/testing/run_tests.py
index d5dd747..4a21171 100755
--- a/testing/run_tests.py
+++ b/testing/run_tests.py
@@ -225,14 +225,17 @@
   assert os.path.exists(kernel_file_output)
 
 
-def RunDartTest(build_dir, dart_file, verbose_dart_snapshot, multithreaded):
+def RunDartTest(build_dir, dart_file, verbose_dart_snapshot, multithreaded, enable_observatory=False):
   kernel_file_name = os.path.basename(dart_file) + '.kernel.dill'
   kernel_file_output = os.path.join(out_dir, kernel_file_name)
 
   SnapshotTest(build_dir, dart_file, kernel_file_output, verbose_dart_snapshot)
 
-  command_args = [
-    '--disable-observatory',
+  command_args = []
+  if not enable_observatory:
+    command_args.append('--disable-observatory')
+
+  command_args += [
     '--use-test-fonts',
     kernel_file_output
   ]
@@ -415,8 +418,18 @@
   # Now that we have the Sky packages at the hardcoded location, run `pub get`.
   RunEngineExecutable(build_dir, os.path.join('dart-sdk', 'bin', 'pub'), None, flags=['get'], cwd=dart_tests_dir)
 
+  dart_observatory_tests = glob.glob('%s/observatory/*_test.dart' % dart_tests_dir)
   dart_tests = glob.glob('%s/*_test.dart' % dart_tests_dir)
 
+  if 'release' not in build_dir:
+    for dart_test_file in dart_observatory_tests:
+      if filter is not None and os.path.basename(dart_test_file) not in filter:
+        print("Skipping %s due to filter." % dart_test_file)
+      else:
+        print("Testing dart file %s with observatory enabled" % dart_test_file)
+        RunDartTest(build_dir, dart_test_file, verbose_dart_snapshot, True, True)
+        RunDartTest(build_dir, dart_test_file, verbose_dart_snapshot, False, True)
+
   for dart_test_file in dart_tests:
     if filter is not None and os.path.basename(dart_test_file) not in filter:
       print("Skipping %s due to filter." % dart_test_file)