From 30911a248553f4fd3b223449a434253a37589b9c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 19 Feb 2026 12:16:01 -0500 Subject: [PATCH] [google_maps_flutter] Improve iOS shared code validation Improves the script that validates that the iOS implementation packages are sync based on early usage: - Skip .DS_Store files when checking shared code. - If the main validation passes, check that the other implementation packages match the shared code, since otherwise manual changes to one package and shared code will, incorrectly, pass CI due to CI skipping unchanged packages. Fixes https://github.com/flutter/flutter/issues/182510 --- .../tool/run_tests.dart | 155 +++++++++++++----- .../tool/run_tests.dart | 155 +++++++++++++----- .../tool/run_tests.dart | 155 +++++++++++++----- .../tool/run_tests.dart | 155 +++++++++++++----- 4 files changed, 452 insertions(+), 168 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/tool/run_tests.dart b/packages/google_maps_flutter/google_maps_flutter_ios/tool/run_tests.dart index f86778c0c2a1..f49c29eaf7fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/tool/run_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/tool/run_tests.dart @@ -33,45 +33,12 @@ Future main(List args) async { p.join(packageRoot.parent.path, 'google_maps_flutter_ios_shared_code'), ); - var failed = false; - for (final FileSystemEntity entity in sharedSourceRoot.listSync( - recursive: true, - )) { - if (entity is! File) { - continue; - } - final String relativePath = p.relative( - entity.path, - from: sharedSourceRoot.path, - ); - // The shared source README.md is not part of the shared source of truth, - // just an explanation of this source-sharing system. - if (relativePath == 'README.md') { - continue; - } - - // Adjust the paths to account for the package name being part of the - // directory structure for Swift packages. - final String packagePath = p.join( - packageRoot.path, - packageRelativePathForSharedSourceRelativePath(packageName, relativePath), - ); - - print('Validating $relativePath'); - final packageFile = File(packagePath); - if (!packageFile.existsSync()) { - print(' File $relativePath does not exist in $packageName'); - failed = true; - continue; - } - final String expectedContents = normalizedFileContents(entity); - final String contents = normalizedFileContents(packageFile); - if (contents != expectedContents) { - print(' File $relativePath does not match expected contents:'); - await _printDiff(entity, packageFile); - failed = true; - } - } + bool passesValidation = await _validatePackageSharedCode( + packageRoot, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: true, + ); print( '\nChecking for unshared source files that are not in ' @@ -85,13 +52,13 @@ Future main(List args) async { if (unsharedFiles.isEmpty) { print(' No unexpected unshared files.'); } else { - failed = true; + passesValidation = false; for (final file in unsharedFiles) { print(' $file is not shared'); } } - if (failed) { + if (!passesValidation) { print(''' If the changes you made should be shared with other copies of the @@ -105,11 +72,115 @@ approach to sharing as much code as can still be shared. For more information on the code sharing structure used by this package, see the google_maps_flutter_ios_shared_code/README.md file. - '''); +'''); + exit(1); + } + + // If everything else passed, sanity-check the other implementation packages. + // Full diff evaluation is done by the copy of this script in each + // implementation package, but if someone edits the shared code without + // updating the other packages, this will catch it. This is useful both + // locally, where people are unlikely to run the tests in every package, and + // in CI, where if there are no changes to an implementation package the CI + // for that package will be skipped. + print('\nChecking other implementation packages...'); + final failingPackages = []; + for (final FileSystemEntity entity in packageRoot.parent.listSync()) { + final String packageName = p.basename(entity.path); + if (entity is! Directory || + !packageName.startsWith('google_maps_flutter_ios_') || + packageName == 'google_maps_flutter_ios_shared_code') { + continue; + } + if (!await _validatePackageSharedCode( + entity, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: false, + )) { + failingPackages.add(packageName); + } + } + + if (failingPackages.isEmpty) { + print(' No unexpected diffs found.'); + } else { + print(''' + The following packages do not match the shared source code: +${failingPackages.map((p) => ' $p').join('\n')} + +If you manually synchronized changes to the shared code, you will also need to +copy those changes to the other implementation packages. In the future, consider +using sync_shared_files.dart instead of copying changes to the shared source +manually. +'''); exit(1); } } +/// Validates that the shared code in [packageRoot] matches the shared source of +/// truth. +/// +/// Returns true if the package matches the shared source of truth. +Future _validatePackageSharedCode( + Directory packageRoot, + String packageName, { + required Directory sharedSourceRoot, + required bool log, +}) async { + var hasDiffs = false; + for (final FileSystemEntity entity in sharedSourceRoot.listSync( + recursive: true, + )) { + if (entity is! File) { + continue; + } + final String relativePath = p.relative( + entity.path, + from: sharedSourceRoot.path, + ); + // The shared source README.md is not part of the shared source of truth, + // just an explanation of this source-sharing system. + if (relativePath == 'README.md') { + continue; + } + // Ignore .DS_Store files, which may be created in the shared source + // directory by the OS. + if (relativePath.endsWith('.DS_Store')) { + continue; + } + + // Adjust the paths to account for the package name being part of the + // directory structure for Swift packages. + final String packagePath = p.join( + packageRoot.path, + packageRelativePathForSharedSourceRelativePath(packageName, relativePath), + ); + + if (log) { + print('Validating $relativePath'); + } + final packageFile = File(packagePath); + if (!packageFile.existsSync()) { + if (log) { + print(' File $relativePath does not exist in $packageName'); + } + hasDiffs = true; + continue; + } + final String expectedContents = normalizedFileContents(entity); + final String contents = normalizedFileContents(packageFile); + if (contents != expectedContents) { + if (log) { + print(' File $relativePath does not match expected contents:'); + await _printDiff(entity, packageFile); + } + hasDiffs = true; + } + } + return !hasDiffs; +} + Future _printDiff(File expected, File actual) async { final Process process = await Process.start('diff', [ '-u', diff --git a/packages/google_maps_flutter/google_maps_flutter_ios_sdk10/tool/run_tests.dart b/packages/google_maps_flutter/google_maps_flutter_ios_sdk10/tool/run_tests.dart index f86778c0c2a1..f49c29eaf7fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios_sdk10/tool/run_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios_sdk10/tool/run_tests.dart @@ -33,45 +33,12 @@ Future main(List args) async { p.join(packageRoot.parent.path, 'google_maps_flutter_ios_shared_code'), ); - var failed = false; - for (final FileSystemEntity entity in sharedSourceRoot.listSync( - recursive: true, - )) { - if (entity is! File) { - continue; - } - final String relativePath = p.relative( - entity.path, - from: sharedSourceRoot.path, - ); - // The shared source README.md is not part of the shared source of truth, - // just an explanation of this source-sharing system. - if (relativePath == 'README.md') { - continue; - } - - // Adjust the paths to account for the package name being part of the - // directory structure for Swift packages. - final String packagePath = p.join( - packageRoot.path, - packageRelativePathForSharedSourceRelativePath(packageName, relativePath), - ); - - print('Validating $relativePath'); - final packageFile = File(packagePath); - if (!packageFile.existsSync()) { - print(' File $relativePath does not exist in $packageName'); - failed = true; - continue; - } - final String expectedContents = normalizedFileContents(entity); - final String contents = normalizedFileContents(packageFile); - if (contents != expectedContents) { - print(' File $relativePath does not match expected contents:'); - await _printDiff(entity, packageFile); - failed = true; - } - } + bool passesValidation = await _validatePackageSharedCode( + packageRoot, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: true, + ); print( '\nChecking for unshared source files that are not in ' @@ -85,13 +52,13 @@ Future main(List args) async { if (unsharedFiles.isEmpty) { print(' No unexpected unshared files.'); } else { - failed = true; + passesValidation = false; for (final file in unsharedFiles) { print(' $file is not shared'); } } - if (failed) { + if (!passesValidation) { print(''' If the changes you made should be shared with other copies of the @@ -105,11 +72,115 @@ approach to sharing as much code as can still be shared. For more information on the code sharing structure used by this package, see the google_maps_flutter_ios_shared_code/README.md file. - '''); +'''); + exit(1); + } + + // If everything else passed, sanity-check the other implementation packages. + // Full diff evaluation is done by the copy of this script in each + // implementation package, but if someone edits the shared code without + // updating the other packages, this will catch it. This is useful both + // locally, where people are unlikely to run the tests in every package, and + // in CI, where if there are no changes to an implementation package the CI + // for that package will be skipped. + print('\nChecking other implementation packages...'); + final failingPackages = []; + for (final FileSystemEntity entity in packageRoot.parent.listSync()) { + final String packageName = p.basename(entity.path); + if (entity is! Directory || + !packageName.startsWith('google_maps_flutter_ios_') || + packageName == 'google_maps_flutter_ios_shared_code') { + continue; + } + if (!await _validatePackageSharedCode( + entity, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: false, + )) { + failingPackages.add(packageName); + } + } + + if (failingPackages.isEmpty) { + print(' No unexpected diffs found.'); + } else { + print(''' + The following packages do not match the shared source code: +${failingPackages.map((p) => ' $p').join('\n')} + +If you manually synchronized changes to the shared code, you will also need to +copy those changes to the other implementation packages. In the future, consider +using sync_shared_files.dart instead of copying changes to the shared source +manually. +'''); exit(1); } } +/// Validates that the shared code in [packageRoot] matches the shared source of +/// truth. +/// +/// Returns true if the package matches the shared source of truth. +Future _validatePackageSharedCode( + Directory packageRoot, + String packageName, { + required Directory sharedSourceRoot, + required bool log, +}) async { + var hasDiffs = false; + for (final FileSystemEntity entity in sharedSourceRoot.listSync( + recursive: true, + )) { + if (entity is! File) { + continue; + } + final String relativePath = p.relative( + entity.path, + from: sharedSourceRoot.path, + ); + // The shared source README.md is not part of the shared source of truth, + // just an explanation of this source-sharing system. + if (relativePath == 'README.md') { + continue; + } + // Ignore .DS_Store files, which may be created in the shared source + // directory by the OS. + if (relativePath.endsWith('.DS_Store')) { + continue; + } + + // Adjust the paths to account for the package name being part of the + // directory structure for Swift packages. + final String packagePath = p.join( + packageRoot.path, + packageRelativePathForSharedSourceRelativePath(packageName, relativePath), + ); + + if (log) { + print('Validating $relativePath'); + } + final packageFile = File(packagePath); + if (!packageFile.existsSync()) { + if (log) { + print(' File $relativePath does not exist in $packageName'); + } + hasDiffs = true; + continue; + } + final String expectedContents = normalizedFileContents(entity); + final String contents = normalizedFileContents(packageFile); + if (contents != expectedContents) { + if (log) { + print(' File $relativePath does not match expected contents:'); + await _printDiff(entity, packageFile); + } + hasDiffs = true; + } + } + return !hasDiffs; +} + Future _printDiff(File expected, File actual) async { final Process process = await Process.start('diff', [ '-u', diff --git a/packages/google_maps_flutter/google_maps_flutter_ios_sdk9/tool/run_tests.dart b/packages/google_maps_flutter/google_maps_flutter_ios_sdk9/tool/run_tests.dart index f86778c0c2a1..f49c29eaf7fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios_sdk9/tool/run_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios_sdk9/tool/run_tests.dart @@ -33,45 +33,12 @@ Future main(List args) async { p.join(packageRoot.parent.path, 'google_maps_flutter_ios_shared_code'), ); - var failed = false; - for (final FileSystemEntity entity in sharedSourceRoot.listSync( - recursive: true, - )) { - if (entity is! File) { - continue; - } - final String relativePath = p.relative( - entity.path, - from: sharedSourceRoot.path, - ); - // The shared source README.md is not part of the shared source of truth, - // just an explanation of this source-sharing system. - if (relativePath == 'README.md') { - continue; - } - - // Adjust the paths to account for the package name being part of the - // directory structure for Swift packages. - final String packagePath = p.join( - packageRoot.path, - packageRelativePathForSharedSourceRelativePath(packageName, relativePath), - ); - - print('Validating $relativePath'); - final packageFile = File(packagePath); - if (!packageFile.existsSync()) { - print(' File $relativePath does not exist in $packageName'); - failed = true; - continue; - } - final String expectedContents = normalizedFileContents(entity); - final String contents = normalizedFileContents(packageFile); - if (contents != expectedContents) { - print(' File $relativePath does not match expected contents:'); - await _printDiff(entity, packageFile); - failed = true; - } - } + bool passesValidation = await _validatePackageSharedCode( + packageRoot, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: true, + ); print( '\nChecking for unshared source files that are not in ' @@ -85,13 +52,13 @@ Future main(List args) async { if (unsharedFiles.isEmpty) { print(' No unexpected unshared files.'); } else { - failed = true; + passesValidation = false; for (final file in unsharedFiles) { print(' $file is not shared'); } } - if (failed) { + if (!passesValidation) { print(''' If the changes you made should be shared with other copies of the @@ -105,11 +72,115 @@ approach to sharing as much code as can still be shared. For more information on the code sharing structure used by this package, see the google_maps_flutter_ios_shared_code/README.md file. - '''); +'''); + exit(1); + } + + // If everything else passed, sanity-check the other implementation packages. + // Full diff evaluation is done by the copy of this script in each + // implementation package, but if someone edits the shared code without + // updating the other packages, this will catch it. This is useful both + // locally, where people are unlikely to run the tests in every package, and + // in CI, where if there are no changes to an implementation package the CI + // for that package will be skipped. + print('\nChecking other implementation packages...'); + final failingPackages = []; + for (final FileSystemEntity entity in packageRoot.parent.listSync()) { + final String packageName = p.basename(entity.path); + if (entity is! Directory || + !packageName.startsWith('google_maps_flutter_ios_') || + packageName == 'google_maps_flutter_ios_shared_code') { + continue; + } + if (!await _validatePackageSharedCode( + entity, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: false, + )) { + failingPackages.add(packageName); + } + } + + if (failingPackages.isEmpty) { + print(' No unexpected diffs found.'); + } else { + print(''' + The following packages do not match the shared source code: +${failingPackages.map((p) => ' $p').join('\n')} + +If you manually synchronized changes to the shared code, you will also need to +copy those changes to the other implementation packages. In the future, consider +using sync_shared_files.dart instead of copying changes to the shared source +manually. +'''); exit(1); } } +/// Validates that the shared code in [packageRoot] matches the shared source of +/// truth. +/// +/// Returns true if the package matches the shared source of truth. +Future _validatePackageSharedCode( + Directory packageRoot, + String packageName, { + required Directory sharedSourceRoot, + required bool log, +}) async { + var hasDiffs = false; + for (final FileSystemEntity entity in sharedSourceRoot.listSync( + recursive: true, + )) { + if (entity is! File) { + continue; + } + final String relativePath = p.relative( + entity.path, + from: sharedSourceRoot.path, + ); + // The shared source README.md is not part of the shared source of truth, + // just an explanation of this source-sharing system. + if (relativePath == 'README.md') { + continue; + } + // Ignore .DS_Store files, which may be created in the shared source + // directory by the OS. + if (relativePath.endsWith('.DS_Store')) { + continue; + } + + // Adjust the paths to account for the package name being part of the + // directory structure for Swift packages. + final String packagePath = p.join( + packageRoot.path, + packageRelativePathForSharedSourceRelativePath(packageName, relativePath), + ); + + if (log) { + print('Validating $relativePath'); + } + final packageFile = File(packagePath); + if (!packageFile.existsSync()) { + if (log) { + print(' File $relativePath does not exist in $packageName'); + } + hasDiffs = true; + continue; + } + final String expectedContents = normalizedFileContents(entity); + final String contents = normalizedFileContents(packageFile); + if (contents != expectedContents) { + if (log) { + print(' File $relativePath does not match expected contents:'); + await _printDiff(entity, packageFile); + } + hasDiffs = true; + } + } + return !hasDiffs; +} + Future _printDiff(File expected, File actual) async { final Process process = await Process.start('diff', [ '-u', diff --git a/packages/google_maps_flutter/google_maps_flutter_ios_shared_code/tool/run_tests.dart b/packages/google_maps_flutter/google_maps_flutter_ios_shared_code/tool/run_tests.dart index f86778c0c2a1..f49c29eaf7fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios_shared_code/tool/run_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios_shared_code/tool/run_tests.dart @@ -33,45 +33,12 @@ Future main(List args) async { p.join(packageRoot.parent.path, 'google_maps_flutter_ios_shared_code'), ); - var failed = false; - for (final FileSystemEntity entity in sharedSourceRoot.listSync( - recursive: true, - )) { - if (entity is! File) { - continue; - } - final String relativePath = p.relative( - entity.path, - from: sharedSourceRoot.path, - ); - // The shared source README.md is not part of the shared source of truth, - // just an explanation of this source-sharing system. - if (relativePath == 'README.md') { - continue; - } - - // Adjust the paths to account for the package name being part of the - // directory structure for Swift packages. - final String packagePath = p.join( - packageRoot.path, - packageRelativePathForSharedSourceRelativePath(packageName, relativePath), - ); - - print('Validating $relativePath'); - final packageFile = File(packagePath); - if (!packageFile.existsSync()) { - print(' File $relativePath does not exist in $packageName'); - failed = true; - continue; - } - final String expectedContents = normalizedFileContents(entity); - final String contents = normalizedFileContents(packageFile); - if (contents != expectedContents) { - print(' File $relativePath does not match expected contents:'); - await _printDiff(entity, packageFile); - failed = true; - } - } + bool passesValidation = await _validatePackageSharedCode( + packageRoot, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: true, + ); print( '\nChecking for unshared source files that are not in ' @@ -85,13 +52,13 @@ Future main(List args) async { if (unsharedFiles.isEmpty) { print(' No unexpected unshared files.'); } else { - failed = true; + passesValidation = false; for (final file in unsharedFiles) { print(' $file is not shared'); } } - if (failed) { + if (!passesValidation) { print(''' If the changes you made should be shared with other copies of the @@ -105,11 +72,115 @@ approach to sharing as much code as can still be shared. For more information on the code sharing structure used by this package, see the google_maps_flutter_ios_shared_code/README.md file. - '''); +'''); + exit(1); + } + + // If everything else passed, sanity-check the other implementation packages. + // Full diff evaluation is done by the copy of this script in each + // implementation package, but if someone edits the shared code without + // updating the other packages, this will catch it. This is useful both + // locally, where people are unlikely to run the tests in every package, and + // in CI, where if there are no changes to an implementation package the CI + // for that package will be skipped. + print('\nChecking other implementation packages...'); + final failingPackages = []; + for (final FileSystemEntity entity in packageRoot.parent.listSync()) { + final String packageName = p.basename(entity.path); + if (entity is! Directory || + !packageName.startsWith('google_maps_flutter_ios_') || + packageName == 'google_maps_flutter_ios_shared_code') { + continue; + } + if (!await _validatePackageSharedCode( + entity, + packageName, + sharedSourceRoot: sharedSourceRoot, + log: false, + )) { + failingPackages.add(packageName); + } + } + + if (failingPackages.isEmpty) { + print(' No unexpected diffs found.'); + } else { + print(''' + The following packages do not match the shared source code: +${failingPackages.map((p) => ' $p').join('\n')} + +If you manually synchronized changes to the shared code, you will also need to +copy those changes to the other implementation packages. In the future, consider +using sync_shared_files.dart instead of copying changes to the shared source +manually. +'''); exit(1); } } +/// Validates that the shared code in [packageRoot] matches the shared source of +/// truth. +/// +/// Returns true if the package matches the shared source of truth. +Future _validatePackageSharedCode( + Directory packageRoot, + String packageName, { + required Directory sharedSourceRoot, + required bool log, +}) async { + var hasDiffs = false; + for (final FileSystemEntity entity in sharedSourceRoot.listSync( + recursive: true, + )) { + if (entity is! File) { + continue; + } + final String relativePath = p.relative( + entity.path, + from: sharedSourceRoot.path, + ); + // The shared source README.md is not part of the shared source of truth, + // just an explanation of this source-sharing system. + if (relativePath == 'README.md') { + continue; + } + // Ignore .DS_Store files, which may be created in the shared source + // directory by the OS. + if (relativePath.endsWith('.DS_Store')) { + continue; + } + + // Adjust the paths to account for the package name being part of the + // directory structure for Swift packages. + final String packagePath = p.join( + packageRoot.path, + packageRelativePathForSharedSourceRelativePath(packageName, relativePath), + ); + + if (log) { + print('Validating $relativePath'); + } + final packageFile = File(packagePath); + if (!packageFile.existsSync()) { + if (log) { + print(' File $relativePath does not exist in $packageName'); + } + hasDiffs = true; + continue; + } + final String expectedContents = normalizedFileContents(entity); + final String contents = normalizedFileContents(packageFile); + if (contents != expectedContents) { + if (log) { + print(' File $relativePath does not match expected contents:'); + await _printDiff(entity, packageFile); + } + hasDiffs = true; + } + } + return !hasDiffs; +} + Future _printDiff(File expected, File actual) async { final Process process = await Process.start('diff', [ '-u',