test generated code is up to date
diff --git a/pkgs/objective_c/test/generate_code_test.dart b/pkgs/objective_c/test/generate_code_test.dart index 1f7a64a..0427cf3 100644 --- a/pkgs/objective_c/test/generate_code_test.dart +++ b/pkgs/objective_c/test/generate_code_test.dart
@@ -9,47 +9,120 @@ import 'dart:io'; import 'package:native_test_helpers/native_test_helpers.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import '../tool/ffigen.dart' as ffigen_tool; +void _matchFileWithExpected(Uri expectedUri, Uri actualUri) { + final expectedPath = expectedUri.toFilePath(); + final expectedFile = File(expectedPath); + final actualPath = actualUri.toFilePath(); + final actualFile = File(actualPath); + + final actualFileExists = actualFile.existsSync(); + final actualContent = actualFileExists + ? actualFile.readAsStringSync().replaceAll('\r', '') + : null; + + final expectedFileExists = expectedFile.existsSync(); + final expectedContent = expectedFileExists + ? expectedFile.readAsStringSync().replaceAll('\r', '') + : null; + + if (expectedContent != actualContent) { + final String diff; + if (expectedFileExists && actualFileExists) { + diff = Process.runSync('git', [ + 'diff', + '--no-index', + '--color=always', + expectedPath, + actualPath, + ]).stdout.toString(); + } else if (expectedFileExists) { + diff = "Expected file exists, but actual file doesn't"; + } else { + diff = "Expected file doesn't exist, but actual file does"; + } + + fail(''' +$diff + +Expected output does not match actual output: + ${p.relative(expectedPath)} + vs + ${p.relative(actualPath)} + +Regenerate bindings with `dart run pkgs/objective_c/tool/ffigen.dart`. +'''); + } +} + void main() { group('ffigen.dart', () { test('Runs without exception', () async { final packageRoot = findPackageRoot('objective_c'); - // As well as testing that this returns normally, this also generates - // coverage info for the parts of FFIgen that are gated by - // generate-for-package-objective-c. The github workflow that runs this - // test also uses it to verify that there are no git-diffs in the output. - await expectLater( - ffigen_tool.run(format: true, packageRoot: packageRoot), - completes, - ); + final tempDir = Directory.fromUri( + packageRoot.resolve('test/'), + ).createTempSync(); + try { + // As well as testing that the generated code is up to date, this also + // generates coverage info for the parts of FFIgen that are gated by + // generate-for-package-objective-c. + await expectLater( + ffigen_tool.run( + format: true, + packageRoot: packageRoot, + outDir: tempDir.uri, + ), + completes, + ); - // Sanity check the generated code. - final rBindings = File.fromUri( - packageRoot.resolve('lib/src/runtime_bindings_generated.dart'), - ).readAsStringSync(); - expect(rBindings, contains('sel_registerName')); - expect(rBindings, contains('objc_msgSend')); - expect(rBindings, contains('_NSConcreteGlobalBlock')); - expect(rBindings, contains('ObjCObject')); + // Sanity check the generated code. + final rBindings = File.fromUri( + tempDir.uri.resolve(ffigen_tool.runtimeBindings), + ).readAsStringSync(); + expect(rBindings, contains('sel_registerName')); + expect(rBindings, contains('objc_msgSend')); + expect(rBindings, contains('_NSConcreteGlobalBlock')); + expect(rBindings, contains('ObjCObject')); - final cBindings = File.fromUri( - packageRoot.resolve('lib/src/c_bindings_generated.dart'), - ).readAsStringSync(); - expect(cBindings, contains('createContext')); - expect(cBindings, contains('DOBJC_Context')); - expect(cBindings, contains('Dart_FinalizableHandle')); - expect(cBindings, contains('ILLEGAL_PORT')); - expect(cBindings, contains('ObjCBlockImpl')); + final cBindings = File.fromUri( + tempDir.uri.resolve(ffigen_tool.cBindings), + ).readAsStringSync(); + expect(cBindings, contains('createContext')); + expect(cBindings, contains('DOBJC_Context')); + expect(cBindings, contains('Dart_FinalizableHandle')); + expect(cBindings, contains('ILLEGAL_PORT')); + expect(cBindings, contains('ObjCBlockImpl')); - final objcBindings = File.fromUri( - packageRoot.resolve('lib/src/objective_c_bindings_generated.dart'), - ).readAsStringSync(); - expect(objcBindings, contains('class NSObject')); - expect(objcBindings, contains('class NSString')); - expect(objcBindings, contains('NSString(String str)')); + final objcBindings = File.fromUri( + tempDir.uri.resolve(ffigen_tool.objcBindings), + ).readAsStringSync(); + expect(objcBindings, contains('class NSObject')); + expect(objcBindings, contains('class NSString')); + expect(objcBindings, contains('NSString(String str)')); + + for (final file in [ + ffigen_tool.runtimeBindings, + ffigen_tool.cBindings, + ffigen_tool.objcBindings, + ffigen_tool.objcLangBindings, + ffigen_tool.objcExports, + ]) { + _matchFileWithExpected( + packageRoot.resolve(file), + tempDir.uri.resolve(file), + ); + } + _matchFileWithExpected( + packageRoot.resolve('../${ffigen_tool.builtInTypes}'), + tempDir.uri.resolve(ffigen_tool.builtInTypes), + ); + } finally { + tempDir.deleteSync(recursive: true); + } }); }); }
diff --git a/pkgs/objective_c/tool/ffigen.dart b/pkgs/objective_c/tool/ffigen.dart index 18a700b..ce1cd7c 100644 --- a/pkgs/objective_c/tool/ffigen.dart +++ b/pkgs/objective_c/tool/ffigen.dart
@@ -11,11 +11,15 @@ import 'package:ffigen/ffigen.dart'; import 'package:logging/logging.dart'; import 'package:native_test_helpers/native_test_helpers.dart'; +import 'package:path/path.dart' as p; const assetId = 'package:objective_c/objective_c.dylib'; const runtimeBindings = 'lib/src/runtime_bindings_generated.dart'; const cBindings = 'lib/src/c_bindings_generated.dart'; const objcBindings = 'lib/src/objective_c_bindings_generated.dart'; +const objcLangBindings = 'src/objective_c_bindings_generated.m'; +const objcExports = 'lib/src/objective_c_bindings_exported.dart'; +const builtInTypes = 'ffigen/lib/src/code_generator/objc_built_in_types.dart'; const objcInterfaces = { 'DOBJCDartInputStreamAdapter': 'DartInputStreamAdapter', @@ -187,35 +191,59 @@ await run(format: argResults.flag('format')); } -Future<void> run({required bool format, Uri? packageRoot}) async { +Future<void> run({required bool format, Uri? packageRoot, Uri? outDir}) async { packageRoot ??= _defaultPackageRoot(); + final outRoot = outDir ?? packageRoot; final logger = Logger.root; logger.level = Level.SEVERE; - final runtimeBindingsPath = packageRoot.resolve(runtimeBindings).toFilePath(); - final cBindingsPath = packageRoot.resolve(cBindings).toFilePath(); - final objcBindingsPath = packageRoot.resolve(objcBindings).toFilePath(); - final objcExportsPath = packageRoot - .resolve('lib/src/objective_c_bindings_exported.dart') - .toFilePath(); + final runtimeBindingsPath = outRoot.resolve(runtimeBindings).toFilePath(); + final cBindingsPath = outRoot.resolve(cBindings).toFilePath(); + final objcBindingsPath = outRoot.resolve(objcBindings).toFilePath(); + final objcLangBindingsPath = outRoot.resolve(objcLangBindings).toFilePath(); + final objcExportsPath = outRoot.resolve(objcExports).toFilePath(); final extraMethodsFilePath = packageRoot .resolve('tool/data/extra_methods.dart.in') .toFilePath(); - final builtInTypesPath = packageRoot - .resolve('../ffigen/lib/src/code_generator/objc_built_in_types.dart') - .toFilePath(); + final builtInTypesPath = + (outDir != null + ? outDir.resolve(builtInTypes) + : packageRoot.resolve('../$builtInTypes')) + .toFilePath(); final interfaceListTestPath = packageRoot .resolve('test/interface_lists_test.dart') .toFilePath(); print('Generating runtime bindings...'); - await getRuntimeConfig(packageRoot).generate(logger: logger); + await getRuntimeConfig( + packageRoot: packageRoot, + outDir: outRoot, + ).generate(logger: logger); print('Generating C bindings...'); - await getCConfig(packageRoot).generate(logger: logger); + await getCConfig( + packageRoot: packageRoot, + outDir: outRoot, + ).generate(logger: logger); print('Generating ObjC bindings...'); - await getObjCConfig(packageRoot).generate(logger: logger); + await getObjCConfig( + packageRoot: packageRoot, + outDir: outRoot, + ).generate(logger: logger); + if (outDir != null) { + final relPrefix = p.relative( + packageRoot.resolve('src').toFilePath(), + from: outRoot.resolve('src').toFilePath(), + ); + final mFile = File(objcLangBindingsPath); + mFile.writeAsStringSync( + mFile.readAsStringSync().replaceAll( + '#import "$relPrefix${p.separator}', + '#import "', + ), + ); + } mergeExtraMethods(objcBindingsPath, parseExtraMethods(extraMethodsFilePath)); print('Generating objc_built_in_types.dart...'); @@ -236,11 +264,13 @@ ], workingDir: packageRoot.toFilePath()); } - print('Running tests...'); - dartCmd([ - 'test', - interfaceListTestPath, - ], workingDir: packageRoot.toFilePath()); + if (outDir == null) { + print('Running tests...'); + dartCmd([ + 'test', + interfaceListTestPath, + ], workingDir: packageRoot.toFilePath()); + } } List<String> writeBuiltInTypes(String out) { @@ -284,13 +314,17 @@ exports.addAll([for (final name in objcProtocols.values) '$name\$Builder']); writeSet('objCBuiltInCategories', objcCategories); - File(out).writeAsStringSync(s.toString()); + File(out) + ..createSync(recursive: true) + ..writeAsStringSync(s.toString()); return exports.toList()..sort(); } void writeExports(List<String> exports, String out) { - File(out).writeAsStringSync(''' + File(out) + ..createSync(recursive: true) + ..writeAsStringSync(''' // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. @@ -303,12 +337,13 @@ '''); } -FfiGenerator getRuntimeConfig([Uri? packageRoot]) { +FfiGenerator getRuntimeConfig({Uri? packageRoot, Uri? outDir}) { packageRoot ??= _defaultPackageRoot(); + outDir ??= packageRoot; return FfiGenerator( output: Output( - dart: DartOutput(path: packageRoot.resolve(runtimeBindings)), + dart: DartOutput(path: outDir.resolve(runtimeBindings)), style: const NativeExternalBindings(), format: false, preamble: ''' @@ -388,12 +423,13 @@ ); } -FfiGenerator getCConfig([Uri? packageRoot]) { +FfiGenerator getCConfig({Uri? packageRoot, Uri? outDir}) { packageRoot ??= _defaultPackageRoot(); + outDir ??= packageRoot; return FfiGenerator( output: Output( - dart: DartOutput(path: packageRoot.resolve(cBindings)), + dart: DartOutput(path: outDir.resolve(cBindings)), style: const NativeExternalBindings(assetId: assetId), format: false, preamble: ''' @@ -461,14 +497,13 @@ ); } -FfiGenerator getObjCConfig([Uri? packageRoot]) { +FfiGenerator getObjCConfig({Uri? packageRoot, Uri? outDir}) { packageRoot ??= _defaultPackageRoot(); + outDir ??= packageRoot; return FfiGenerator( output: Output( - dart: DartOutput(path: packageRoot.resolve(objcBindings)), - objectiveCFile: packageRoot.resolve( - 'src/objective_c_bindings_generated.m', - ), + dart: DartOutput(path: outDir.resolve(objcBindings)), + objectiveCFile: outDir.resolve(objcLangBindings), style: const NativeExternalBindings(assetId: assetId), format: false, preamble: '''