Fix tools test verifyVersion() regex (#41744)

diff --git a/.cirrus.yml b/.cirrus.yml
index 738bf1b..ba8185c 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -59,6 +59,13 @@
     - name: analyze
       test_script:
         - dart --enable-asserts ./dev/bots/analyze.dart
+    - name: bots_tests-linux
+      skip: "!changesInclude('dev/bots/**')"
+      test_script:
+        - (cd ./dev/bots && pub run test)
+      container:
+        cpu: 4
+        memory: 12G
     - name: tests_widgets-linux
       skip: "!changesInclude('packages/flutter/**', 'packages/flutter_test/**', 'packages/flutter_tools/lib/src/test/**', 'bin/internal/**') && $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH != 'stable' && $CIRRUS_BRANCH != 'beta' && $CIRRUS_BRANCH != 'dev'"
       env:
@@ -320,6 +327,11 @@
     - flutter update-packages
     - git fetch origin master
   matrix:
+    - name: bots_tests-windows
+      skip: "!changesInclude('dev/bots/**')"
+      test_script:
+        - cd dev\bots
+        - pub run test
     - name: tests_widgets-windows
       skip: "!changesInclude('packages/flutter/**', 'packages/flutter_test/**', 'packages/flutter_tools/lib/src/test/**', 'bin/internal/**') && $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH != 'stable' && $CIRRUS_BRANCH != 'beta' && $CIRRUS_BRANCH != 'dev'"
       env:
@@ -496,6 +508,10 @@
     - bin/flutter doctor -v
     - bin/flutter update-packages
   matrix:
+    - name: bots_tests-macos
+      skip: "!changesInclude('dev/bots/**')"
+      test_script:
+        - (cd ./dev/bots && pub run test)
     - name: tests_widgets-macos
       only_if: $CIRRUS_BRANCH == 'master'
       env:
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 9ba8cb7..2beb892 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -8,6 +8,7 @@
 import 'package:googleapis/bigquery/v2.dart' as bq;
 import 'package:googleapis_auth/auth_io.dart' as auth;
 import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
 
 import 'flutter_compact_formatter.dart';
@@ -153,7 +154,10 @@
   );
 
   // Verify that we correctly generated the version file.
-  await _verifyVersion(path.join(flutterRoot, 'version'));
+  final bool validVersion = await verifyVersion(path.join(flutterRoot, 'version'));
+  if (!validVersion) {
+    exit(1);
+  }
 }
 
 Future<bq.BigqueryApi> _getBigqueryApi() async {
@@ -924,27 +928,31 @@
   }
 }
 
-Future<void> _verifyVersion(String filename) async {
-  if (!File(filename).existsSync()) {
+// the optional `file` argument is an override for testing
+@visibleForTesting
+Future<bool> verifyVersion(String filename, [File file]) async {
+  final RegExp pattern = RegExp(r'^\d+\.\d+\.\d+(\+hotfix\.\d+)?(-pre\.\d+)?$');
+  file ??= File(filename);
+  final String version = await file.readAsString();
+  if (!file.existsSync()) {
     print('$redLine');
     print('The version logic failed to create the Flutter version file.');
     print('$redLine');
-    exit(1);
+    return false;
   }
-  final String version = await File(filename).readAsString();
   if (version == '0.0.0-unknown') {
     print('$redLine');
     print('The version logic failed to determine the Flutter version.');
     print('$redLine');
-    exit(1);
+    return false;
   }
-  final RegExp pattern = RegExp(r'^\d+\.\d+\.\d+(?:|-pre\.\d+|\+hotfix\.\d+)$');
   if (!version.contains(pattern)) {
     print('$redLine');
-    print('The version logic generated an invalid version string.');
+    print('The version logic generated an invalid version string: "$version".');
     print('$redLine');
-    exit(1);
+    return false;
   }
+  return true;
 }
 
 Future<void> _runIntegrationTests() async {
diff --git a/dev/bots/test/test_test.dart b/dev/bots/test/test_test.dart
new file mode 100644
index 0000000..a3abf6a
--- /dev/null
+++ b/dev/bots/test/test_test.dart
@@ -0,0 +1,51 @@
+// Copyright 2017 The Chromium 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:async';
+import 'dart:io' hide Platform;
+
+import 'package:mockito/mockito.dart';
+
+import '../test.dart';
+import 'common.dart';
+
+class MockFile extends Mock implements File {}
+
+void main() {
+  MockFile file;
+  setUp(() {
+    file = MockFile();
+    when(file.existsSync()).thenReturn(true);
+  });
+  group('verifyVersion()', () {
+    test('passes for valid version strings', () async {
+      const List<String> valid_versions = <String>[
+        '1.2.3',
+        '12.34.56',
+        '1.2.3-pre.1',
+        '1.2.3+hotfix.1',
+        '1.2.3+hotfix.12-pre.12',
+      ];
+      for (String version in valid_versions) {
+        when(file.readAsString()).thenAnswer((Invocation invocation) => Future<String>.value(version));
+        expect(await verifyVersion(version, file), isTrue, reason: '$version is invalid');
+      }
+    });
+
+    test('fails for invalid version strings', () async {
+      const List<String> invalid_versions = <String>[
+        '1.2.3.4',
+        '1.2.3.',
+        '1.2-pre.1',
+        '1.2.3-pre',
+        '1.2.3-pre.1+hotfix.1',
+        '  1.2.3',
+      ];
+      for (String version in invalid_versions) {
+        when(file.readAsString()).thenAnswer((Invocation invocation) => Future<String>.value(version));
+        expect(await verifyVersion(version, file), isFalse);
+      }
+    });
+  });
+}