From 92dd82f49c5d0a522497ef4b53896c21b75d95a7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 12 Aug 2023 22:03:58 +0200 Subject: [PATCH] Restore cache on a per-element basis --- drift_dev/CHANGELOG.md | 5 + drift_dev/lib/src/analysis/driver/cache.dart | 6 +- drift_dev/lib/src/analysis/driver/driver.dart | 117 ++++++---------- .../lib/src/analysis/resolver/resolver.dart | 41 ++++-- drift_dev/lib/src/analysis/serializer.dart | 51 ++++--- .../lib/src/backends/build/analyzer.dart | 6 +- drift_dev/pubspec.yaml | 2 + .../queries/custom_class_name_test.dart | 2 +- .../build/build_integration_test.dart | 132 ++++++++++++++++-- .../backends/build/drift_builder_test.dart | 2 +- drift_dev/test/utils.dart | 114 +++++++++++++-- .../test/writer/data_class_writer_test.dart | 10 +- .../writer/function_stubs_writer_test.dart | 6 +- .../mutable_classes_integration_test.dart | 2 +- .../writer/queries/query_writer_test.dart | 8 +- drift_dev/test/writer/table_writer_test.dart | 2 +- drift_dev/test/writer/writer_test.dart | 4 +- 17 files changed, 360 insertions(+), 150 deletions(-) diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index 53c6e2244..119d0352e 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.11.1-dev + +- Reduce the amount of assets read by drift, improving build performance and enabling faster + incremental rebuilds. + ## 2.11.0 - [Nested result columns](https://drift.simonbinder.eu/docs/using-sql/drift_files/#nested-results) diff --git a/drift_dev/lib/src/analysis/driver/cache.dart b/drift_dev/lib/src/analysis/driver/cache.dart index f7da94200..744f6c9a1 100644 --- a/drift_dev/lib/src/analysis/driver/cache.dart +++ b/drift_dev/lib/src/analysis/driver/cache.dart @@ -1,3 +1,5 @@ +import 'package:analyzer/dart/element/element.dart'; + import '../results/element.dart'; import 'state.dart'; @@ -5,10 +7,12 @@ import 'state.dart'; /// /// At the moment, the cache is not set up to handle changing files. class DriftAnalysisCache { - final Map serializationCache = {}; + final Map serializationCache = {}; final Map knownFiles = {}; final Map discoveredElements = {}; + final Map typeHelperLibraries = {}; + FileState stateForUri(Uri uri) { return knownFiles[uri] ?? notifyFileChanged(uri); } diff --git a/drift_dev/lib/src/analysis/driver/driver.dart b/drift_dev/lib/src/analysis/driver/driver.dart index 96aeb7763..cdf0d2fba 100644 --- a/drift_dev/lib/src/analysis/driver/driver.dart +++ b/drift_dev/lib/src/analysis/driver/driver.dart @@ -12,7 +12,6 @@ import '../resolver/drift/sqlparser/mapping.dart'; import '../resolver/file_analysis.dart'; import '../resolver/queries/custom_known_functions.dart'; import '../resolver/resolver.dart'; -import '../results/results.dart'; import '../serializer.dart'; import 'cache.dart'; import 'error.dart'; @@ -59,7 +58,6 @@ class DriftAnalysisDriver { final bool _isTesting; late final TypeMapping typeMapping = TypeMapping(this); - late final ElementDeserializer deserializer = ElementDeserializer(this); AnalysisResultCacheReader? cacheReader; @@ -100,8 +98,9 @@ class DriftAnalysisDriver { /// /// Returns non-null if analysis results were found and successfully restored. Future?> readStoredAnalysisResult(Uri uri) async { - final cached = cache.serializationCache[uri]; - if (cached != null) return cached.cachedElements; + if (cache.serializationCache.containsKey(uri)) { + return cache.serializationCache[uri]?.cachedElements; + } // Not available in in-memory cache, so let's read it from the file system. final reader = cacheReader; @@ -122,41 +121,6 @@ class DriftAnalysisDriver { return data.cachedElements; } - Future _recoverFromCache(FileState state) async { - final stored = await readStoredAnalysisResult(state.ownUri); - if (stored == null) return false; - - var allRecovered = true; - - for (final local in stored.keys) { - final id = DriftElementId(state.ownUri, local); - try { - await deserializer.readDriftElement(id); - } on CouldNotDeserializeException catch (e, s) { - backend.log.fine('Could not deserialize $id', e, s); - allRecovered = false; - } - } - - final cachedImports = cache.serializationCache[state.ownUri]?.cachedImports; - if (cachedImports != null && state.discovery == null) { - state.cachedDiscovery ??= CachedDiscoveryResults(true, cachedImports, []); - - for (final import in cachedImports) { - final found = cache.stateForUri(import); - - if (found.imports == null) { - // Attempt to recover this file as well to make sure we know the - // imports for every file transitively reachable from the sources - // analyzed. - await _recoverFromCache(found); - } - } - } - - return allRecovered; - } - Future discoverIfNecessary( FileState file, { bool warnIfFileDoesntExist = true, @@ -208,15 +172,10 @@ class DriftAnalysisDriver { return known; } - Future _findLocalElementsInAllImports(FileState known) async { - // To analyze references in elements, we also need to know locally defined - // elements in all imports. + Future _warnAboutUnresolvedImportsInDriftFile(FileState known) async { final state = known.discovery; if (state is DiscoveredDriftFile) { for (final import in state.imports) { - // todo: We shouldn't unconditionally crawl files like this. The build - // backend should emit prepared file results in a previous step which - // should be used here. final file = await findLocalElements(import.importedUri); if (file.isValidImport != true) { @@ -227,37 +186,26 @@ class DriftAnalysisDriver { "can't be imported.", ), ); - } else { - await _findLocalElementsInAllImports(file); } } - } else { - for (final import in known.imports ?? const []) { - await findLocalElements( - import, - // We might import a generated file that doesn't exist yet, that - // should not be a user-visible error. Users will notice because the - // import is reported as an error by the analyzer either way. - warnIfFileDoesntExist: false, - ); - } } } - /// Runs the second analysis step (element analysis) on a file. + /// Analyzes elements known to be defined in [state], or restores them from + /// cache. /// - /// The file, as well as all imports, should have undergone the first analysis - /// step (discovery) at this point, so that the resolver is able to - /// recognize dependencies between different elements. - Future _analyzePrepared(FileState state) async { - assert(state.discovery != null); + /// Elements in the file must be known at this point - either because the file + /// was discovered or because discovered elements have been imported from + /// cache. + Future _analyzeLocalElements(FileState state) async { + assert(state.discovery != null || state.cachedDiscovery != null); - for (final discovered in state.discovery!.locallyDefinedElements) { + for (final discovered in state.definedElements) { if (!state.elementIsAnalyzed(discovered.ownId)) { final resolver = DriftResolver(this); try { - await resolver.resolveDiscovered(discovered); + await resolver.resolveEntrypoint(discovered.ownId); } catch (e, s) { if (e is! CouldNotResolveElementException) { backend.log.warning('Could not analyze ${discovered.ownId}', e, s); @@ -278,19 +226,38 @@ class DriftAnalysisDriver { return known; } - final allRecoveredFromCache = await _recoverFromCache(known); - if (allRecoveredFromCache) { - // We were able to read all elements from cache, so we don't have to - // run any analysis now. - return known; - } - // We couldn't recover all analyzed elements. Let's run an analysis run // then. - await discoverIfNecessary(known); - await _findLocalElementsInAllImports(known); + await findLocalElements(uri); + await _warnAboutUnresolvedImportsInDriftFile(known); + + // Also make sure elements in transitive imports have been resolved. + final seen = cache.knownFiles.keys.toSet(); + final pending = [known.ownUri]; + + while (pending.isNotEmpty) { + final file = pending.removeLast(); + seen.add(file); + + final fileState = await findLocalElements( + file, + // We might import a generated file that doesn't exist yet, that + // should not be a user-visible error. Users will notice because the + // import is reported as an error by the analyzer either way. + warnIfFileDoesntExist: true, + ); + + // Drift file imports are transitive, Dart imports aren't. + if (file == known.ownUri || fileState.extension == '.drift') { + for (final dependency in fileState.imports ?? const []) { + if (!seen.contains(dependency)) { + pending.add(dependency); + } + } + } + } - await _analyzePrepared(known); + await _analyzeLocalElements(known); return known; } diff --git a/drift_dev/lib/src/analysis/resolver/resolver.dart b/drift_dev/lib/src/analysis/resolver/resolver.dart index 0419e84c1..c5861d97a 100644 --- a/drift_dev/lib/src/analysis/resolver/resolver.dart +++ b/drift_dev/lib/src/analysis/resolver/resolver.dart @@ -6,6 +6,7 @@ import '../driver/error.dart'; import '../driver/state.dart'; import '../results/element.dart'; +import '../serializer.dart'; import 'dart/accessor.dart' as dart_accessor; import 'dart/table.dart' as dart_table; import 'dart/view.dart' as dart_view; @@ -25,10 +26,38 @@ class DriftResolver { /// This path is used to detect and prevent circular references. final List _currentDependencyPath = []; + late final ElementDeserializer _deserializer = + ElementDeserializer(driver, _currentDependencyPath); + DriftResolver(this.driver); + Future resolveEntrypoint(DriftElementId element) async { + assert(_currentDependencyPath.isEmpty); + _currentDependencyPath.add(element); + + return await _restoreOrResolve(element); + } + + Future _restoreOrResolve(DriftElementId element) async { + try { + if (await driver.readStoredAnalysisResult(element.libraryUri) != null) { + return await _deserializer.readDriftElement(element); + } + } on CouldNotDeserializeException catch (e, s) { + driver.backend.log.fine('Could not deserialize $element', e, s); + } + + // We can't resolve the element from cache, so we need to resolve it. + final owningFile = driver.cache.stateForUri(element.libraryUri); + await driver.discoverIfNecessary(owningFile); + final discovered = owningFile.discovery!.locallyDefinedElements + .firstWhere((e) => e.ownId == element); + + return _resolveDiscovered(discovered); + } + /// Resolves a discovered element by analyzing it and its dependencies. - Future resolveDiscovered(DiscoveredElement discovered) async { + Future _resolveDiscovered(DiscoveredElement discovered) async { LocalElementResolver resolver; final fileState = driver.cache.knownFiles[discovered.ownId.libraryUri]!; @@ -111,18 +140,14 @@ class DriftResolver { final pending = driver.cache.discoveredElements[reference]; if (pending != null) { + // We know the element exists, but we haven't resolved it yet. _currentDependencyPath.add(reference); try { - final owningFile = driver.cache.stateForUri(reference.libraryUri); - await driver.discoverIfNecessary(owningFile); - final discovered = owningFile.discovery!.locallyDefinedElements - .firstWhere((e) => e.ownId == reference); - - final resolved = await resolveDiscovered(discovered); + final resolved = await _restoreOrResolve(reference); return ResolvedReferenceFound(resolved); } catch (e, s) { - driver.backend.log.warning('Could not analze $reference', e, s); + driver.backend.log.warning('Could not analyze $reference', e, s); return ReferencedElementCouldNotBeResolved(); } finally { final removed = _currentDependencyPath.removeLast(); diff --git a/drift_dev/lib/src/analysis/serializer.dart b/drift_dev/lib/src/analysis/serializer.dart index b37ad797e..8a9b0e64c 100644 --- a/drift_dev/lib/src/analysis/serializer.dart +++ b/drift_dev/lib/src/analysis/serializer.dart @@ -358,20 +358,21 @@ class ElementSerializer { /// Deserializes the element structure emitted by [ElementSerializer]. class ElementDeserializer { - final Map _typeHelperLibraries = {}; - final List _currentlyReading = []; + final List _currentlyReading; final DriftAnalysisDriver driver; - ElementDeserializer(this.driver); + ElementDeserializer(this.driver, this._currentlyReading); Future _readDartType(Uri import, int typeId) async { LibraryElement? element; - if (_typeHelperLibraries.containsKey(import)) { - element = _typeHelperLibraries[import]; + final helpers = driver.cache.typeHelperLibraries; + + if (helpers.containsKey(import)) { + element = helpers[import]; } else { - element = _typeHelperLibraries[import] = - await driver.cacheReader!.readTypeHelperFor(import); + element = + helpers[import] = await driver.cacheReader!.readTypeHelperFor(import); } if (element == null) { @@ -383,11 +384,28 @@ class ElementDeserializer { return typedef.aliasedType; } - Future _readElementReference(Map json) { - return readDriftElement(DriftElementId.fromJson(json)); + Future _readElementReference(Map json) async { + final id = DriftElementId.fromJson(json); + + if (_currentlyReading.contains(id)) { + throw StateError( + 'Circular error when deserializing drift modules. This is a ' + 'bug in drift_dev!'); + } + + _currentlyReading.add(id); + + try { + return await readDriftElement(DriftElementId.fromJson(json)); + } finally { + final lastId = _currentlyReading.removeLast(); + assert(lastId == id); + } } Future readDriftElement(DriftElementId id) async { + assert(_currentlyReading.last == id); + final state = driver.cache.stateForUri(id.libraryUri).analysis[id] ??= ElementAnalysisState(id); if (state.result != null && state.isUpToDate) { @@ -397,18 +415,10 @@ class ElementDeserializer { final data = await driver.readStoredAnalysisResult(id.libraryUri); if (data == null) { throw CouldNotDeserializeException( - 'Analysis data for ${id..libraryUri} not found'); - } - - if (_currentlyReading.contains(id)) { - throw StateError( - 'Circular error when deserializing drift modules. This is a ' - 'bug in drift_dev!'); + 'Analysis data for ${id.libraryUri} not found'); } try { - _currentlyReading.add(id); - final result = await _readDriftElement(data[id.name] as Map); state ..result = result @@ -419,9 +429,6 @@ class ElementDeserializer { throw CouldNotDeserializeException( 'Internal error while deserializing $id: $e at \n$s'); - } finally { - final lastId = _currentlyReading.removeLast(); - assert(lastId == id); } } @@ -665,7 +672,7 @@ class ElementDeserializer { schemaVersion: json['schema_version'] as int?, accessors: [ for (final dao in json.list('daos')) - await readDriftElement(DriftElementId.fromJson(dao as Map)) + await _readElementReference(dao as Map) as DatabaseAccessor, ], ); diff --git a/drift_dev/lib/src/backends/build/analyzer.dart b/drift_dev/lib/src/backends/build/analyzer.dart index b9ebe0200..a21dbfbd7 100644 --- a/drift_dev/lib/src/backends/build/analyzer.dart +++ b/drift_dev/lib/src/backends/build/analyzer.dart @@ -51,7 +51,7 @@ class DriftDiscover extends Builder { 'kind': entry.kind.name, 'name': entry.ownId.name, if (entry is DiscoveredDartElement) - 'dart_name:': entry.dartElement.name, + 'dart_name': entry.dartElement.name, } ] }), @@ -88,6 +88,10 @@ class DriftAnalyzer extends Builder { final results = await driver.resolveElements(buildStep.inputId.uri); var hadWarnings = false; + // The discovery builder is just here to accelerate builds and doesn't + // print errors found during discovery. To ensure that we're starting a + // fresh discovery run here, call it explicitly. + await driver.discoverIfNecessary(results); for (final parseError in results.errorsDuringDiscovery) { log.warning(parseError.toString()); hadWarnings = true; diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index dcf378e79..ddd3f0e4a 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -57,6 +57,8 @@ dev_dependencies: build_runner: ^2.0.0 build_test: ^2.0.0 json_serializable: ^6.2.0 + crypto: ^3.0.3 + glob: ^2.1.2 executables: drift_dev: diff --git a/drift_dev/test/analysis/resolver/queries/custom_class_name_test.dart b/drift_dev/test/analysis/resolver/queries/custom_class_name_test.dart index ffd8bd8d9..fd405922b 100644 --- a/drift_dev/test/analysis/resolver/queries/custom_class_name_test.dart +++ b/drift_dev/test/analysis/resolver/queries/custom_class_name_test.dart @@ -113,7 +113,7 @@ getTitlesWithGroupOther AS GroupWithTitles: SELECT group_name, LIST(SELECT title 'contains one GroupWithTitles class', hasLength(1), )), - }, results.dartOutputs, results); + }, results.dartOutputs, results.writer); }); test('supports query with two list columns', () async { diff --git a/drift_dev/test/backends/build/build_integration_test.dart b/drift_dev/test/backends/build/build_integration_test.dart index a9e0a7dde..0c15131f3 100644 --- a/drift_dev/test/backends/build/build_integration_test.dart +++ b/drift_dev/test/backends/build/build_integration_test.dart @@ -1,6 +1,9 @@ import 'package:build/build.dart'; import 'package:build_test/build_test.dart'; +import 'package:drift_dev/src/backends/build/analyzer.dart'; +import 'package:drift_dev/src/backends/build/drift_builder.dart'; import 'package:drift_dev/src/backends/build/exception.dart'; +import 'package:drift_dev/src/backends/build/preprocess_builder.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; @@ -27,7 +30,7 @@ CREATE INDEX b_idx /* comment should be stripped */ ON b (foo); checkOutputs({ 'a|lib/main.drift.dart': decodedMatches(contains( "late final Index bIdx = Index('b_idx', 'CREATE INDEX b_idx ON b (foo)')")), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('warns about errors in imports', () async { @@ -104,7 +107,7 @@ selectAll: SELECT * FROM foo; checkOutputs({ 'a|lib/database.drift.dart': decodedMatches(contains('selectAll')), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('can work with existing part files', () async { @@ -136,7 +139,7 @@ class Users extends Table { checkOutputs( {'a|lib/main.drift.dart': decodedMatches(contains('class User'))}, result.dartOutputs, - result, + result.writer, ); }); @@ -170,7 +173,7 @@ class MyDatabase {} logger: logger, ); - checkOutputs({}, result.dartOutputs, result); + checkOutputs({}, result.dartOutputs, result.writer); }); test('generates custom result classes with modular generation', () async { @@ -192,7 +195,7 @@ secondQuery AS MyResultClass: SELECT 'bar' AS r1, 2 AS r2; 'a|lib/main.drift.dart': decodedMatches(predicate((String generated) { return 'class MyResultClass'.allMatches(generated).length == 1; })), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('generates imports for query variables with modular generation', @@ -240,7 +243,7 @@ q: INSERT INTO my_table (b, c, d) VALUES (?, ?, ?); ), ), ), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('supports `MAPPED BY` for columns', () async { @@ -272,7 +275,7 @@ class ADrift extends i1.ModularAccessor { } } ''')), - }, results.dartOutputs, results); + }, results.dartOutputs, results.writer); }); test('generates type converters for views', () async { @@ -317,7 +320,7 @@ TypeConverter myConverter() => throw UnimplementedError(); ), }, result.dartOutputs, - result, + result.writer, ); }); @@ -346,7 +349,7 @@ class Database extends $Database {} 'a|lib/a.drift.dart': decodedMatches(contains(r'OnCreateQuery get $drift0 => ')), 'a|lib/db.drift.dart': decodedMatches(contains(r'.$drift0];')) - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('writes query from transitive import', () async { @@ -379,7 +382,7 @@ q: SELECT 1; 'a|lib/main.drift.dart': decodedMatches( contains(r'Selectable q()'), ) - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('warns when Dart tables are included', () async { @@ -425,7 +428,114 @@ class MyDatabase {} 'a|lib/main.drift.dart': decodedMatches( startsWith('// generated by drift\n'), ), - }, outputs.dartOutputs, outputs); + }, outputs.dartOutputs, outputs.writer); + }); + + test('does not read unecessary files', () async { + final inputs = { + 'a|lib/groups.drift': ''' +CREATE TABLE "groups" ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL +); +''', + 'a|lib/members.drift': ''' +import 'groups.drift'; +import 'database.dart'; + +CREATE TABLE memberships ( + "group" INTEGER NOT NULL REFERENCES "groups"(id), + "user" INTEGER NOT NULL REFERENCES "users" (id), + PRIMARY KEY ("group", user) +); +''', + 'a|lib/database.dart': ''' +import 'package:drift/drift.dart'; + +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); +} + +@DriftDatabase(include: {'groups.drift', 'members.drift'}) +class MyDatabase { + +} +''', + }; + final outputs = await emulateDriftBuild(inputs: inputs); + final readAssets = outputs.readAssetsByBuilder; + + Matcher onlyReadsJsonsAnd(dynamic other) { + return everyElement( + anyOf( + isA().having((e) => e.extension, 'extension', '.json'), + other, + ), + ); + } + + void expectReadsForBuilder(String input, Type builder, dynamic expected) { + final actuallyRead = readAssets.remove((builder, input)); + expect(actuallyRead, expected); + } + + // 1. Preprocess builders read only the drift file itself and no other + // files. + for (final input in inputs.keys) { + if (input.endsWith('.drift')) { + expectReadsForBuilder(input, PreprocessBuilder, [makeAssetId(input)]); + } + } + + // The discover builder needs to analyze Dart files, which in the current + // resolver implementation means reading all transitive imports as well. + // However, the discover builder should not read other drift files. + for (final input in inputs.keys) { + if (input.endsWith('.drift')) { + expectReadsForBuilder(input, DriftDiscover, [makeAssetId(input)]); + } else { + expectReadsForBuilder( + input, + DriftDiscover, + isNot( + contains( + isA().having((e) => e.extension, 'extension', '.drift'), + ), + ), + ); + } + } + + // Groups has no imports, so the analyzer shouldn't read any source files + // apart from groups. + expectReadsForBuilder('a|lib/groups.drift', DriftAnalyzer, + onlyReadsJsonsAnd(makeAssetId('a|lib/groups.drift'))); + + // Members is analyzed next. We don't have analysis results for the dart + // file yet, so unfortunately that will have to be analyzed twice. But we + // shouldn't read groups again. + expectReadsForBuilder('a|lib/members.drift', DriftAnalyzer, + isNot(contains(makeAssetId('a|lib/groups.drift')))); + + // Similarly, analyzing the Dart file should not read the includes since + // those have already been analyzed. + expectReadsForBuilder( + 'a|lib/database.dart', + DriftAnalyzer, + isNot( + contains( + isA().having((e) => e.extension, 'extension', '.drift'), + ), + ), + ); + + // The final builder needs to run file analysis which requires resolving + // the input file fully. Unfortunately, resolving queries also needs access + // to the original source so there's not really anything we could test. + expectReadsForBuilder('a|lib/database.dart', DriftBuilder, anything); + + // Make sure we didn't forget an assertion. + expect(readAssets, isEmpty); }); group('reports issues', () { diff --git a/drift_dev/test/backends/build/drift_builder_test.dart b/drift_dev/test/backends/build/drift_builder_test.dart index eb4eaccf1..1486dfa3e 100644 --- a/drift_dev/test/backends/build/drift_builder_test.dart +++ b/drift_dev/test/backends/build/drift_builder_test.dart @@ -47,7 +47,7 @@ class Database {} 'foo|lib/a.drift.dart': decodedMatches(contains('// @dart=2.13')), }, writer.dartOutputs, - writer, + writer.writer, ); }); } diff --git a/drift_dev/test/utils.dart b/drift_dev/test/utils.dart index 3d176ef37..3bb2f3337 100644 --- a/drift_dev/test/utils.dart +++ b/drift_dev/test/utils.dart @@ -1,9 +1,15 @@ +import 'dart:convert'; +import 'dart:isolate'; + import 'package:build/build.dart'; import 'package:build/experiments.dart'; import 'package:build_resolvers/build_resolvers.dart'; import 'package:build_test/build_test.dart'; +import 'package:crypto/crypto.dart'; import 'package:drift_dev/integrations/build.dart'; +import 'package:glob/glob.dart'; import 'package:logging/logging.dart'; +import 'package:package_config/package_config.dart'; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; @@ -22,7 +28,19 @@ Logger loggerThat(dynamic expectedLogs) { return logger; } -Future emulateDriftBuild({ +final _packageConfig = Future(() async { + final uri = await Isolate.packageConfig; + + if (uri == null) { + throw UnsupportedError( + 'Isolate running the build does not have a package config and no ' + 'fallback has been provided'); + } + + return await loadPackageConfigUri(uri); +}); + +Future emulateDriftBuild({ required Map inputs, BuilderOptions options = const BuilderOptions({}), Logger? logger, @@ -42,10 +60,11 @@ Future emulateDriftBuild({ ), await PackageAssetReader.currentIsolate(), ]); + final readAssets = <(Type, String), Set>{}; final stages = [ - discover(options), preparingBuilder(options), + discover(options), analyzer(options), modularBuild ? modular(options) : driftBuilderNotShared(options), driftCleanup(options), @@ -53,14 +72,28 @@ Future emulateDriftBuild({ for (final stage in stages) { if (stage is Builder) { - await runBuilder( - stage, - inputs.keys.map(makeAssetId), - reader, - writer, - _resolvers, - logger: logger, - ); + // We might want to consider running these concurrently, but tests are + // easier to debug when running builders in a serial order. + for (final input in inputs.keys) { + final inputId = makeAssetId(input); + + if (expectedOutputs(stage, inputId).isNotEmpty) { + final readerForPhase = _TrackingAssetReader(reader); + + await runBuilder( + stage, + [inputId], + readerForPhase, + writer, + _resolvers, + logger: logger, + packageConfig: await _packageConfig, + ); + + readAssets.putIfAbsent( + (stage.runtimeType, input), () => {}).addAll(readerForPhase.read); + } + } } else if (stage is PostProcessBuilder) { final deleted = []; @@ -84,13 +117,68 @@ Future emulateDriftBuild({ } logger.clearListeners(); - return writer; + return DriftBuildResult(writer, readAssets); } -extension OnlyDartOutputs on RecordingAssetWriter { +class DriftBuildResult { + final InMemoryAssetWriter writer; + + /// Asset ids read for each (builder, input id) combination. + final Map<(Type, String), Set> readAssetsByBuilder; + + DriftBuildResult(this.writer, this.readAssetsByBuilder); + Iterable get dartOutputs { - return assets.keys.where((e) { + return writer.assets.keys.where((e) { return e.extension == '.dart'; }); } + + void checkDartOutputs(Map outputs) { + checkOutputs(outputs, dartOutputs, writer); + } +} + +class _TrackingAssetReader implements AssetReader { + final AssetReader _inner; + + final Set read = {}; + + _TrackingAssetReader(this._inner); + + void _trackRead(AssetId id) { + read.add(id); + } + + @override + Future canRead(AssetId id) { + _trackRead(id); + return _inner.canRead(id); + } + + @override + Future digest(AssetId id) { + _trackRead(id); + return _inner.digest(id); + } + + @override + Stream findAssets(Glob glob) { + return _inner.findAssets(glob).map((id) { + _trackRead(id); + return id; + }); + } + + @override + Future> readAsBytes(AssetId id) { + _trackRead(id); + return _inner.readAsBytes(id); + } + + @override + Future readAsString(AssetId id, {Encoding encoding = utf8}) { + _trackRead(id); + return _inner.readAsString(id, encoding: encoding); + } } diff --git a/drift_dev/test/writer/data_class_writer_test.dart b/drift_dev/test/writer/data_class_writer_test.dart index bbd41d3df..c2f4aec6a 100644 --- a/drift_dev/test/writer/data_class_writer_test.dart +++ b/drift_dev/test/writer/data_class_writer_test.dart @@ -38,7 +38,7 @@ class Database extends _$Database {} 'a|lib/main.drift.dart': _GeneratesConstDataClasses( {'User', 'UsersCompanion'}, ), - }, writer.dartOutputs, writer); + }, writer.dartOutputs, writer.writer); }, tags: 'analyzer', ); @@ -87,7 +87,7 @@ class Database extends _$Database {} ); } ''')), - }, writer.dartOutputs, writer); + }, writer.dartOutputs, writer.writer); }, tags: 'analyzer', ); @@ -120,7 +120,7 @@ class Database extends _$Database {} 'a|lib/main.drift.dart': decodedMatches(contains(r''' static JsonTypeConverter2 $converterpriority = const EnumNameConverter(Priority.values);''')), - }, writer.dartOutputs, writer); + }, writer.dartOutputs, writer.writer); }, tags: 'analyzer', ); @@ -173,7 +173,7 @@ mixin PostsToColumns implements i1.Insertable { } } ''')), - }, writer.dartOutputs, writer); + }, writer.dartOutputs, writer.writer); }); test('generates correct fromJson for nullable converters', () async { @@ -210,7 +210,7 @@ class MyTable extends Table { .fromJson(serializer.fromJson(json['invoiceContact'])), ); }''')), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); } diff --git a/drift_dev/test/writer/function_stubs_writer_test.dart b/drift_dev/test/writer/function_stubs_writer_test.dart index 19d33f174..e4d5b54c0 100644 --- a/drift_dev/test/writer/function_stubs_writer_test.dart +++ b/drift_dev/test/writer/function_stubs_writer_test.dart @@ -9,7 +9,7 @@ void main() { final logger = Logger.detached('driftBuild'); expect(logger.onRecord, neverEmits(anything)); - final writer = await emulateDriftBuild( + final build = await emulateDriftBuild( inputs: { 'a|lib/a.dart': ''' import 'package:drift/drift.dart'; @@ -37,7 +37,7 @@ sql: logger: logger, ); - checkOutputs( + build.checkDartOutputs( { 'a|lib/a.drift.dart': decodedMatches( contains(''' @@ -68,8 +68,6 @@ extension DefineFunctions on i3.CommonDatabase { ), 'a|lib/queries.drift.dart': anything, }, - writer.dartOutputs, - writer, ); }); } diff --git a/drift_dev/test/writer/mutable_classes_integration_test.dart b/drift_dev/test/writer/mutable_classes_integration_test.dart index 35699f8c2..effb73579 100644 --- a/drift_dev/test/writer/mutable_classes_integration_test.dart +++ b/drift_dev/test/writer/mutable_classes_integration_test.dart @@ -41,7 +41,7 @@ class Database extends _$Database {} ), }, writer.dartOutputs, - writer, + writer.writer, ); }, tags: 'analyzer'); } diff --git a/drift_dev/test/writer/queries/query_writer_test.dart b/drift_dev/test/writer/queries/query_writer_test.dart index c789ddfa4..22f67d56e 100644 --- a/drift_dev/test/writer/queries/query_writer_test.dart +++ b/drift_dev/test/writer/queries/query_writer_test.dart @@ -331,7 +331,7 @@ class MyRow { ''')) }, result.dartOutputs, - result, + result.writer, ); }); @@ -391,7 +391,7 @@ getTest WITH TestCustom: ' testTwoText: row.read(\'test_two_text\'),\n' ' ));\n' ' }')), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); test('generates correct code for variables in LIST subquery', () async { @@ -439,7 +439,7 @@ failQuery: )); } ''')) - }, outputs.dartOutputs, outputs); + }, outputs.dartOutputs, outputs.writer); }); test('supports Dart component in HAVING', () async { @@ -523,7 +523,7 @@ class ADrift extends i1.ModularAccessor { )); } }''')) - }, outputs.dartOutputs, outputs); + }, outputs.dartOutputs, outputs.writer); }); test('creates dialect-specific query code', () async { diff --git a/drift_dev/test/writer/table_writer_test.dart b/drift_dev/test/writer/table_writer_test.dart index 172b3b3d1..07beb2367 100644 --- a/drift_dev/test/writer/table_writer_test.dart +++ b/drift_dev/test/writer/table_writer_test.dart @@ -31,6 +31,6 @@ class Tags extends Table { contains('class MyTag extends i0.MyBaseDataClass'), ), ), - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); } diff --git a/drift_dev/test/writer/writer_test.dart b/drift_dev/test/writer/writer_test.dart index ca417ead0..39807073f 100644 --- a/drift_dev/test/writer/writer_test.dart +++ b/drift_dev/test/writer/writer_test.dart @@ -44,7 +44,7 @@ foo: SELECT foo FROM my_table; contains(r'i2.MyTable.$converterfoo'), ), ) - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }); }); @@ -86,6 +86,6 @@ class Database {} ); '''), )) - }, result.dartOutputs, result); + }, result.dartOutputs, result.writer); }, skip: requireDart('3.0.0-dev')); }