blob: 74eff1420e15b6bca82c857c6db82af084b2a652 [file] [edit]
// 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.
import 'dart:io';
import 'package:ffigen/ffigen.dart' as fg;
import 'package:logging/logging.dart';
import 'package:native_test_helpers/native_test_helpers.dart';
import 'package:path/path.dart' as path;
import 'package:swiftgen/src/util.dart';
import 'package:swiftgen/swiftgen.dart';
import 'package:test/test.dart';
String pkgDir = findPackageRoot('swiftgen').toFilePath();
Future<Target> hostTarget = Target.host();
// There are language features that aren't supported in Swift2ObjC yet, but that
// we want to test in SwiftGen. So for now we write @objc annotated bindings for
// these features. As each of these features is supported, migrate the test to
// stop using hand-written @objc annotations.
// TODO(https://github.com/dart-lang/native/issues/1669): callbacks
// TODO(https://github.com/dart-lang/native/issues/1828): protocols
const objCCompatibleTests = {'callbacks', 'protocols'};
class TestGenerator {
final String name;
late final String testDir;
late final String tempDir;
late final String inputFile;
late final String wrapperFile;
late final String outputFile;
late final String outputObjCFile;
late final String objInputFile;
late final String objWrapperFile;
late final String objObjCFile;
late final String dylibFile;
late final String actualOutputFile;
final bool isObjCCompatible;
TestGenerator(this.name)
: isObjCCompatible = objCCompatibleTests.contains(name) {
testDir = path.absolute(path.join(pkgDir, 'test/integration'));
tempDir = path.join(testDir, 'temp');
inputFile = path.join(testDir, '$name.swift');
wrapperFile = path.join(tempDir, '${name}_wrapper.swift');
outputFile = path.join(tempDir, '${name}_output.dart');
outputObjCFile = path.join(tempDir, '${name}_output.m');
objInputFile = path.join(tempDir, '$name.o');
objWrapperFile = path.join(tempDir, '${name}_wrapper.o');
objObjCFile = path.join(tempDir, '${name}_output_m.o');
dylibFile = path.join(tempDir, '$name.dylib');
actualOutputFile = path.join(testDir, '${name}_bindings.dart');
}
Future<void> generateBindings([Target? target]) async =>
await SwiftGenerator(
target: target ?? await hostTarget,
inputs: [
isObjCCompatible
? ObjCCompatibleSwiftFileInput(files: [Uri.file(inputFile)])
: SwiftFileInput(
files: [Uri.file(inputFile)],
tempModuleName: name,
),
],
output: Output(
swiftWrapperFile: isObjCCompatible
? null
: SwiftWrapperFile(path: Uri.file(wrapperFile)),
module: name,
dartFile: Uri.file(outputFile),
objectiveCFile: Uri.file(outputObjCFile),
preamble: '''
// 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.
// coverage:ignore-file
''',
),
ffigen: FfiGeneratorOptions(
visitors: [
fg.Visitor(
objCInterface: (node) {
if (node.name.startsWith('Test')) {
node.isIncluded = true;
}
},
objCProtocol: (node) {
if (node.name.startsWith('Test')) {
node.isIncluded = true;
}
},
),
],
),
).generate(
logger: Logger.root..level = Level.SEVERE,
tempDirectory: Uri.directory(tempDir),
);
Future<void> generateAndVerifyBindings([Target? compileTarget]) async {
// Run the generation pipeline. This produces the swift compatability
// wrapper, and the ffigen wrapper.
await generateBindings(compileTarget);
expect(File(inputFile).existsSync(), isTrue);
expect(File(wrapperFile).existsSync(), !isObjCCompatible);
expect(File(outputFile).existsSync(), isTrue);
// The generation pipeline also an obj file as a byproduct.
expect(File(objInputFile).existsSync(), isTrue);
final target = compileTarget ?? await hostTarget;
await run('swiftc', [
'-c',
inputFile,
if (!isObjCCompatible) wrapperFile,
'-module-name',
name,
'-target',
target.triple,
'-sdk',
path.absolute(target.sdk.toFilePath()),
], tempDir);
final hasOutputObjCFile = File(outputObjCFile).existsSync();
if (hasOutputObjCFile) {
// We also need to compile outputObjCFile to an obj file.
await run('clang', [
'-x',
'objective-c',
'-fobjc-arc',
'-c',
outputObjCFile,
'-fpic',
'-target',
target.triple,
'-isysroot',
path.absolute(target.sdk.toFilePath()),
'-o',
objObjCFile,
], tempDir);
expect(File(objObjCFile).existsSync(), isTrue);
}
// Link all the obj files into a dylib.
await run('clang', [
'-shared',
'-framework',
'Foundation',
'-target',
target.triple,
'-isysroot',
path.absolute(target.sdk.toFilePath()),
objInputFile,
if (!isObjCCompatible) objWrapperFile,
if (hasOutputObjCFile) objObjCFile,
'-o',
dylibFile,
], tempDir);
expect(File(dylibFile).existsSync(), isTrue);
// Expect that the bindings match.
expect(
File(outputFile).readAsStringSync(),
File(actualOutputFile).readAsStringSync(),
);
}
}