import 'dart:io'; import 'dart:isolate'; import 'package:analyzer/dart/analysis/analysis_context.dart'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/file_system/overlay_file_system.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; import 'package:build/build.dart'; import 'package:drift/drift.dart'; import 'package:drift_dev/src/analysis/backend.dart'; import 'package:drift_dev/src/analysis/driver/driver.dart'; import 'package:drift_dev/src/analysis/driver/error.dart'; import 'package:drift_dev/src/analysis/driver/state.dart'; import 'package:drift_dev/src/analysis/results/results.dart'; import 'package:drift_dev/src/analysis/options.dart'; import 'package:logging/logging.dart'; import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; /// A [DriftBackend] implementation used for testing. /// /// This backend has limited support for Dart analysis: [sourceContents] forming /// the package `a` are available for analysis. In addition, `drift` and /// `drift_dev` imports can be analyzed as well. class TestBackend extends DriftBackend { final Map sourceContents; final Iterable analyzerExperiments; late final DriftAnalysisDriver driver; AnalysisContext? _dartContext; TestBackend( Map sourceContents, { DriftOptions options = const DriftOptions.defaults(), this.analyzerExperiments = const Iterable.empty(), }) : sourceContents = { for (final entry in sourceContents.entries) AssetId.parse(entry.key).uri.toString(): entry.value, } { driver = DriftAnalysisDriver(this, options); } factory TestBackend.inTest( Map sourceContents, { DriftOptions options = const DriftOptions.defaults(), Iterable analyzerExperiments = const Iterable.empty(), }) { final backend = TestBackend(sourceContents, options: options, analyzerExperiments: analyzerExperiments); addTearDown(backend.dispose); return backend; } static Future analyzeSingle(String content, {String asset = 'a|lib/a.drift', DriftOptions options = const DriftOptions.defaults()}) { final assetId = AssetId.parse(asset); final backend = TestBackend.inTest({asset: content}, options: options); return backend.driver.fullyAnalyze(assetId.uri); } void expectNoErrors() { for (final file in driver.cache.knownFiles.values) { expect(file.allErrors, isEmpty, reason: 'Error in ${file.ownUri}'); } } Future _setupDartAnalyzer() async { final provider = OverlayResourceProvider(PhysicalResourceProvider.INSTANCE); // Analyze example sources against the drift sources from the current // drift_dev test runner. final uri = await Isolate.packageConfig; final hostConfig = PackageConfig.parseBytes(await File.fromUri(uri!).readAsBytes(), uri); final testConfig = PackageConfig([ ...hostConfig.packages, Package('a', Uri.directory('/a/'), packageUriRoot: Uri.parse('lib/')), ]); // Write package config used to analyze dummy sources final configBuffer = StringBuffer(); PackageConfig.writeString(testConfig, configBuffer); provider.setOverlay('/a/.dart_tool/package_config.json', content: configBuffer.toString(), modificationStamp: 1); // Also put sources into the overlay: sourceContents.forEach((key, value) { final uri = Uri.parse(key); if (uri.scheme == 'package') { final package = uri.pathSegments.first; final path = p.url.joinAll(['/$package/lib', ...uri.pathSegments.skip(1)]); provider.setOverlay(path, content: value, modificationStamp: 1); } }); if (analyzerExperiments.isNotEmpty) { final experiments = analyzerExperiments.join(', '); provider.setOverlay( '/a/analysis_options.yaml', content: 'analyzer: {enable-experiment: [$experiments]}', modificationStamp: 1, ); } final collection = AnalysisContextCollection( includedPaths: ['/a'], resourceProvider: provider, ); _dartContext = collection.contexts.single; } Future ensureHasDartAnalyzer() async { if (_dartContext == null) { await _setupDartAnalyzer(); } } @override Logger get log => Logger.root; @override Uri resolveUri(Uri base, String uriString) { return base.resolve(uriString); } @override Future readAsString(Uri uri) async { return sourceContents[uri.toString()] ?? (throw StateError('No source for $uri')); } @override Future resolveExpression( Uri context, String dartExpression, Iterable imports) async { throw UnsupportedError('Not currently supported in tests'); } @override Future readDart(Uri uri) async { await ensureHasDartAnalyzer(); final result = await _dartContext!.currentSession.getLibraryByUri(uri.toString()); return (result as LibraryElementResult).element; } @override Future loadElementDeclaration(Element element) async { final library = element.library; if (library == null) return null; final info = await library.session.getResolvedLibraryByElement(library); if (info is ResolvedLibraryResult) { return info.getElementDeclaration(element)?.node; } else { return null; } } Future dispose() async {} Future analyze(String uriString) { return driver.fullyAnalyze(Uri.parse(uriString)); } } Matcher get hasNoErrors => isA().having((e) => e.allErrors, 'allErrors', isEmpty); Matcher returnsColumns(Map columns) { return _HasInferredColumnTypes(columns); } class _HasInferredColumnTypes extends CustomMatcher { _HasInferredColumnTypes(dynamic expected) : super('Select query with inferred columns', 'columns', expected); @override Object? featureValueOf(dynamic actual) { if (actual is! SqlSelectQuery) { return actual; } final resultSet = actual.resultSet; return { for (final column in resultSet.scalarColumns) column.name: column.sqlType }; } } TypeMatcher isDriftError(dynamic message) { return isA().having((e) => e.message, 'message', message); } final _version = RegExp(r'\d\.\d+\.\d+'); String? requireDart(String minimalVersion) { final version = Version.parse(_version.firstMatch(Platform.version)!.group(0)!); final minimal = Version.parse(minimalVersion); if (version < minimal) { return 'This test requires SDK version $minimalVersion or later'; } else { return null; } } extension DriftErrorMatchers on TypeMatcher { TypeMatcher withSpan(lexemeMatcher) { return having((e) => e.span?.text, 'span.text', lexemeMatcher); } }