blob: 5e892eb9a748a9ccff85e188ecaeaa38f4df4c92 [file] [log] [blame] [edit]
// Copyright (c) 2020, 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/src/code_generator.dart';
import 'package:ffigen/src/code_generator/scope.dart';
import 'package:ffigen/src/code_generator/utils.dart';
import 'package:ffigen/src/config_provider/config.dart';
import 'package:ffigen/src/config_provider/utils.dart';
import 'package:ffigen/src/config_provider/yaml_config.dart';
import 'package:ffigen/src/context.dart';
import 'package:ffigen/src/visitor/ast.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config_types.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:yaml/yaml.dart' as yaml;
export 'package:ffigen/src/config_provider/utils.dart';
Context testContext([FfiGenerator? generator]) => Context(
Logger.root,
generator ?? FfiGenerator(output: Output(dartFile: Uri.file('unused'))),
);
extension LibraryTestExt on Library {
/// Get a [Binding]'s generated string with a given name.
String getBindingAsString(String name) =>
getBinding(name).toBindingString(writer).string;
/// Get a [Binding] with a given name.
Binding getBinding(String name) {
try {
final b = bindings.firstWhere((element) => element.name == name);
return b;
} catch (e) {
throw NotFoundException("Binding '$name' not found.");
}
}
/// Runs a fake version of the Symbol renaming step that usually happens
/// during the transformation pipeline. The Symbol names aren't actually
/// renamed to avoid collisions, the name is just filled from oldName without
/// regard for the other names in the namespace. This lets us access the names
/// for testing, without running whole transformation pipeline.
void forceFillNamesForTesting() {
visit(context, _FakeRenamer(), bindings);
context.extraSymbols = (
wrapperClassName: Symbol('NativeLibrary')..forceFillForTesting(),
lookupFuncName: Symbol('_lookup')..forceFillForTesting(),
symbolAddressVariableName: Symbol('addresses')..forceFillForTesting(),
);
context.libs.forceFillForTesting();
context.rootScope.fillNames();
context.rootObjCScope.fillNames();
}
}
class _FakeRenamer extends Visitation {
@override
void visitSymbol(Symbol node) => node.forceFillForTesting();
@override
void visitBinding(Binding node) {
if (node is HasLocalScope) {
(node as HasLocalScope).localScope = Scope.createRoot('test')
..fillNames();
}
node.visitChildren(visitor);
}
}
/// Check whether a file generated by test/setup.dart exists and throw a helpful
/// exception if it does not.
void verifySetupFile(File file) {
if (!file.existsSync()) {
throw NotFoundException(
'The file ${file.path} does not exist.\n\n'
'You may need to run: dart run test/setup.dart\n',
);
}
}
// Remove '\r' for Windows compatibility, then apply user's normalizer.
String _normalizeGeneratedCode(
String generated,
String Function(String)? codeNormalizer,
) {
final noCR = generated.replaceAll('\r', '');
if (codeNormalizer == null) return noCR;
return codeNormalizer(noCR);
}
/// Generates actual file using library and tests using [expect] with expected.
///
/// This will not delete the actual debug file incase [expect] throws an error.
void matchLibraryWithExpected(
Library library,
String pathForActual,
List<String> pathToExpected, {
String Function(String)? codeNormalizer,
bool format = true,
}) {
_matchFileWithExpected(
library: library,
pathForActual: pathForActual,
pathToExpected: pathToExpected,
fileWriter: ({required Library library, required File file}) =>
library.generateFile(file, format: format),
codeNormalizer: codeNormalizer,
);
}
/// Generates actual file using library and tests using [expect] with expected.
///
/// This will not delete the actual debug file incase [expect] throws an error.
void matchLibrarySymbolFileWithExpected(
Library library,
String pathForActual,
List<String> pathToExpected,
String importPath,
) {
_matchFileWithExpected(
library: library,
pathForActual: pathForActual,
pathToExpected: pathToExpected,
fileWriter: ({required Library library, required File file}) {
if (!library.writer.canGenerateSymbolOutput) library.generate();
library.generateSymbolOutputFile(file, importPath);
},
);
}
const bool updateExpectations = true;
/// Transforms a repo relative path to an absolute path.
String absPath(String p) => path.join(packagePathForTests, p);
/// Returns a path to a config yaml in a unit test.
String configPath(String directory, String file) =>
absPath(configPathForTest(directory, file));
/// Returns the temp directory used to store bindings generated by tests.
String tmpDir = path.join(packagePathForTests, 'test', '.temp');
/// Generates actual file using library and tests using [expect] with expected.
///
/// This will not delete the actual debug file incase [expect] throws an error.
void _matchFileWithExpected({
required Library library,
required String pathForActual,
required List<String> pathToExpected,
required void Function({required Library library, required File file})
fileWriter,
String Function(String)? codeNormalizer,
}) {
final expectedPath = path.joinAll([packagePathForTests, ...pathToExpected]);
final file = File(path.join(tmpDir, pathForActual));
fileWriter(library: library, file: file);
try {
final actual = _normalizeGeneratedCode(
file.readAsStringSync(),
codeNormalizer,
);
final expected = _normalizeGeneratedCode(
File(expectedPath).readAsStringSync(),
codeNormalizer,
);
expect(actual.split('\n'), expected.split('\n'));
_expectNoAnalysisErrors(expectedPath);
if (file.existsSync()) {
file.delete();
}
} catch (e) {
print('Failed test: Debug generated file: ${file.absolute.path}');
if (updateExpectations) {
print('Updating expectations. Check the diffs!');
file.copySync(expectedPath);
}
rethrow;
}
}
void _expectNoAnalysisErrors(String file) {
Process.runSync(dartExecutable, [
'pub',
'get',
], workingDirectory: path.dirname(file));
final result = Process.runSync(dartExecutable, [
'analyze',
file,
], workingDirectory: path.dirname(file));
if (result.exitCode != 0) print(result.stdout);
expect(result.exitCode, 0);
}
class NotFoundException implements Exception {
final String message;
NotFoundException(this.message);
@override
String toString() {
return message;
}
}
void logWarnings([Level level = Level.WARNING]) {
Logger.root.level = level;
Logger.root.onRecord.listen((record) {
printOnFailure('${record.level.name.padRight(8)}: ${record.message}');
});
}
Logger logToArray(List<String> logArr, Level level) {
Logger.root.level = level;
Logger.root.onRecord.listen((record) {});
final logger = Logger('ffigen.test');
logger.onRecord.listen((record) {
logArr.add('${record.level.name.padRight(8)}: ${record.message}');
});
return logger;
}
FfiGenerator testConfig(String yamlBody, {String? filename, Logger? logger}) {
return YamlConfig.fromYaml(
yaml.loadYaml(yamlBody) as yaml.YamlMap,
logger ?? Logger.root,
filename: filename,
packageConfig: PackageConfig([
Package(
'shared_bindings',
Uri.file(
path.join(packagePathForTests, 'example', 'shared_bindings', 'lib/'),
),
),
]),
).configAdapter();
}
FfiGenerator testConfigFromPath(String path) {
final file = File(path);
final yamlBody = file.readAsStringSync();
return testConfig(yamlBody, filename: path);
}
T withChDir<T>(String path, T Function() inner) {
final oldDir = Directory.current;
Directory.current = File(path).parent;
late T result;
try {
result = inner();
} finally {
Directory.current = oldDir;
}
return result;
}
bool isFlutterTester = Platform.resolvedExecutable.contains('flutter_tester');