Found by dogfooding on nuwayze: `pipe queries lib` from the repo root
failed with
no resolved package graph at .dart_tool/package_config.json
remedy: run `flutter pub get` or `dart pub get` in .../nuwayze-w3/lib
It failed even with the package resolved, because the path was taken
as the project root: pipe looked for lib/.dart_tool/package_config.json,
which never exists, and the remedy sent the user to run pub get in a
directory that has no pubspec.
enclosingPackage() (project.dart) splits a path into the package that
contains it (the path itself when it holds a pubspec.yaml, else the
nearest ancestor that does) and the directory it names under that
root. `pipe queries` loads the package root and scans only that
directory, so `pipe queries lib/services` is `--dir lib/services` on
the package; an explicit --dir with such a path is relative to it.
Without a resolved graph the error now names the package root.
loadProject's own remedy uses the same lookup, so `pipe check lib`
(and the other commands, which still take the path as the root) now
says the path is inside the package at <root> and suggests passing
that with `--dir lib`, instead of pub get in lib.
Tests: enclosingPackage on a root, a nested directory, a nested
package and a missing path; loadProject's remedy for a directory
inside a package; and `pipe queries` on the fixture's lib/data
(root reported as the package, one file discovered), with
`lib --dir data`, and on an unresolved package where the remedy names
the root, not lib.
On nuwayze-w3: `pipe queries lib` exits 0 with 313 queries over 847
files, identical to `pipe queries .`; `pipe queries lib/services` scans
161 files and every one of its 212 queries sits under lib/services.
233 tests pass.
662 lines
23 KiB
Dart
662 lines
23 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:pipe/pipe.dart';
|
|
import 'package:pipe/src/cli.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
const String fixturePath = 'test/fixtures/firestore_queries';
|
|
const String adapterConfigPath = '$fixturePath/adapters.json';
|
|
|
|
void main() {
|
|
late Project project;
|
|
late ResolutionRun run;
|
|
late FirestoreModel model;
|
|
|
|
setUpAll(() async {
|
|
project = await loadProject(fixturePath);
|
|
run = await resolveProject(project, concurrency: 1);
|
|
model = buildFirestoreModel(
|
|
project,
|
|
run.outcomes,
|
|
adapters: parseAdapterConfig(File(adapterConfigPath).readAsStringSync()),
|
|
);
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
await run.dispose();
|
|
});
|
|
|
|
List<QueryRecord> byMember(String member, {String? collection}) => [
|
|
for (final query in model.queries)
|
|
if (query.member == member &&
|
|
(collection == null || query.collection == collection))
|
|
query,
|
|
];
|
|
|
|
QueryRecord single(String member, {String? collection}) {
|
|
final hits = byMember(member, collection: collection);
|
|
expect(
|
|
hits,
|
|
hasLength(1),
|
|
reason: '$member/${collection ?? '*'}: ${hits.map((q) => q.site)}',
|
|
);
|
|
return hits.single;
|
|
}
|
|
|
|
List<String> ops(QueryRecord query) => [
|
|
for (final filter in query.filters) '${filter.field} ${filter.op}',
|
|
];
|
|
|
|
test('fixture resolves with zero analyzer errors', () {
|
|
// test/fixtures/** has its own analysis_options, so `dart analyze` in the
|
|
// repo does not vouch for the fixture; the analyzer's own diagnostics do.
|
|
final errors = <String>[];
|
|
for (final outcome in run.outcomes) {
|
|
expect(outcome.resolved, isTrue, reason: outcome.failure);
|
|
for (final diagnostic in outcome.diagnostics) {
|
|
if (diagnostic.severity.name != 'error') continue;
|
|
errors.add('${outcome.path}: ${diagnostic.message}');
|
|
}
|
|
}
|
|
expect(errors, isEmpty, reason: errors.join('\n'));
|
|
expect(model.firestoreResolved, isTrue);
|
|
expect(model.packageDeclared, isTrue);
|
|
});
|
|
|
|
group('direct chains', () {
|
|
test('equality plus orderBy is exact', () {
|
|
final query = single('equalityAndOrder');
|
|
expect(query.scope, 'collection');
|
|
expect(query.collection, 'categories');
|
|
expect(query.pathPattern, 'categories');
|
|
expect(ops(query), ['isActive ==']);
|
|
expect(query.filters.single.valueType, 'bool');
|
|
expect(query.orderBy.single.toJson(), {
|
|
'field': 'name',
|
|
'dir': 'asc',
|
|
'conditional': false,
|
|
});
|
|
expect(query.limit.kind, 'none');
|
|
expect(query.realtime, isFalse);
|
|
expect(query.confidence, 'exact');
|
|
expect(query.origin, 'chain');
|
|
expect(query.owner, 'DirectQueries');
|
|
expect(query.reasons, isEmpty);
|
|
});
|
|
|
|
test('every named operator maps to its Firestore operator', () {
|
|
final query = single('everyOperator');
|
|
expect(ops(query), [
|
|
'ownerId ==',
|
|
'status !=',
|
|
'price <',
|
|
'price <=',
|
|
'createdAt >',
|
|
'rank >=',
|
|
'tags array-contains',
|
|
'genres array-contains-any',
|
|
'city in',
|
|
'kind not-in',
|
|
'deletedAt ==',
|
|
'publishedAt !=',
|
|
]);
|
|
final types = {
|
|
for (final filter in query.filters)
|
|
'${filter.field} ${filter.op}': filter.valueType,
|
|
};
|
|
expect(types['ownerId =='], 'string');
|
|
expect(types['price <'], 'int');
|
|
expect(types['price <='], 'double');
|
|
expect(types['createdAt >'], 'timestamp');
|
|
expect(types['genres array-contains-any'], 'list');
|
|
expect(types['deletedAt =='], 'null');
|
|
expect(types['publishedAt !='], 'null');
|
|
final counts = {
|
|
for (final filter in query.filters)
|
|
if (filter.inCount != null) filter.field: filter.inCount,
|
|
};
|
|
expect(counts, {'genres': 2, 'city': 3, 'kind': 1});
|
|
expect(query.limit.toJson(), {
|
|
'kind': 'literal',
|
|
'value': 20,
|
|
'conditional': false,
|
|
});
|
|
});
|
|
|
|
test('two operators in one where() are two filters', () {
|
|
final query = single('rangeInOneCall');
|
|
expect(ops(query), ['startsAt >=', 'startsAt <']);
|
|
expect(query.filters.map((f) => f.valueType), everyElement('timestamp'));
|
|
expect(query.orderBy.single.dir, 'desc');
|
|
});
|
|
|
|
test('collection names resolve through constants', () {
|
|
final names = byMember('constantNames').map((q) => q.collection);
|
|
expect(names, ['orders', 'auditTrail', 'reviews']);
|
|
final audit = single('constantNames', collection: 'auditTrail');
|
|
expect(audit.limit.last, isTrue);
|
|
expect(audit.limit.value, 5);
|
|
expect(byMember('constantNames').map((q) => q.confidence), [
|
|
'exact',
|
|
'exact',
|
|
'exact',
|
|
]);
|
|
});
|
|
|
|
test('interpolated and helper-built paths keep dynamic document ids', () {
|
|
final products = single('interpolatedPath');
|
|
expect(products.pathPattern, 'stores/*/products');
|
|
expect(products.collection, 'products');
|
|
expect(products.confidence, 'exact');
|
|
|
|
final payouts = single('pathHelpers', collection: 'payouts');
|
|
expect(payouts.pathPattern, 'stores/*/payouts');
|
|
final drafts = single('pathHelpers', collection: 'drafts');
|
|
expect(drafts.pathPattern, 'users/*/drafts');
|
|
});
|
|
|
|
test('doc() then collection() builds a subcollection path', () {
|
|
final services = single('documentThenSubcollection');
|
|
expect(services.pathPattern, 'stores/*/services');
|
|
expect(services.collection, 'services');
|
|
expect(services.cursor, isTrue);
|
|
|
|
final saved = single('rootDocumentPath');
|
|
expect(saved.pathPattern, 'users/*/saved');
|
|
});
|
|
|
|
test('collectionGroup with snapshots is a realtime group query', () {
|
|
final query = single('groupStream');
|
|
expect(query.scope, 'collectionGroup');
|
|
expect(query.collection, 'userSubscriptions');
|
|
expect(query.realtime, isTrue);
|
|
});
|
|
|
|
test('FieldPath.documentId is __name__, FieldPath([...]) is dotted', () {
|
|
final query = single('fieldPaths');
|
|
expect(ops(query), ['__name__ ==', 'address.city ==']);
|
|
expect(query.orderBy.single.field, '__name__');
|
|
expect(query.confidence, 'exact');
|
|
});
|
|
|
|
test('Filter.or / Filter.and become a composite node', () {
|
|
final query = single('disjunction');
|
|
expect(query.filters, isEmpty);
|
|
expect(query.composite!.toJson(), {
|
|
'op': 'or',
|
|
'filters': [
|
|
{
|
|
'field': 'buyerId',
|
|
'op': '==',
|
|
'conditional': false,
|
|
'valueType': 'string',
|
|
},
|
|
{
|
|
'composite': {
|
|
'op': 'and',
|
|
'filters': [
|
|
{
|
|
'field': 'sellerId',
|
|
'op': '==',
|
|
'conditional': false,
|
|
'valueType': 'string',
|
|
},
|
|
{
|
|
'field': 'open',
|
|
'op': '==',
|
|
'conditional': false,
|
|
'valueType': 'bool',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('count() and aggregate(sum) are aggregation queries', () {
|
|
final queries = byMember('counting');
|
|
expect(queries.map((q) => q.aggregate), ['count', 'sum']);
|
|
expect(ops(queries.first), ['storeId ==']);
|
|
});
|
|
|
|
test('withConverter passes the query through', () {
|
|
final query = single('converted');
|
|
expect(query.collection, 'tips');
|
|
expect(ops(query), ['amount >']);
|
|
});
|
|
|
|
test('a fully dynamic collection is partial with no collection', () {
|
|
final query = single('dynamicCollection');
|
|
expect(query.collection, isNull);
|
|
expect(query.pathPattern, '*');
|
|
expect(query.confidence, 'partial');
|
|
expect(
|
|
query.reasons,
|
|
contains('collection name is not a compile-time constant'),
|
|
);
|
|
expect(ops(query), ['a ==']);
|
|
});
|
|
|
|
test('document reads are counted, never listed', () {
|
|
expect(byMember('documentRead'), isEmpty);
|
|
expect(model.documentReads, 1);
|
|
expect(model.notes.join(), contains('1 document read'));
|
|
});
|
|
});
|
|
|
|
group('name validation', () {
|
|
test('names outside the safe set are replaced, with a reason', () {
|
|
final query = single('unsafeNames');
|
|
expect(query.pathPattern, '*');
|
|
expect(query.collection, isNull);
|
|
expect(query.filters.single.field, '*');
|
|
expect(query.confidence, 'partial');
|
|
expect(
|
|
query.reasons,
|
|
containsAll([
|
|
'a field name has characters outside the safe set',
|
|
'a path segment has characters outside the safe set',
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('no source text beyond safe names reaches the JSON', () {
|
|
final json = jsonEncode([for (final q in model.queries) q.toJson()]);
|
|
expect(json, isNot(contains('ignore previous')));
|
|
expect(json, isNot(contains('a b')));
|
|
for (final query in model.queries) {
|
|
final names = [
|
|
?query.collection,
|
|
?query.pathPattern,
|
|
for (final filter in query.filters) filter.field,
|
|
for (final order in query.orderBy) order.field,
|
|
];
|
|
for (final name in names) {
|
|
expect(safeNamePattern.hasMatch(name), isTrue, reason: name);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
group('built queries', () {
|
|
test('a variable-built query unions its clauses; ifs make them '
|
|
'conditional', () {
|
|
final query = single('streamUserNotifications');
|
|
expect(query.collection, 'notifications');
|
|
expect(query.origin, 'variable');
|
|
expect(ops(query), ['userId ==']);
|
|
expect(query.filters.single.conditional, isFalse);
|
|
expect(query.orderBy.single.toJson(), {
|
|
'field': 'timestamp',
|
|
'dir': 'desc',
|
|
'conditional': false,
|
|
});
|
|
expect(query.limit.toJson(), {'kind': 'variable', 'conditional': true});
|
|
expect(query.cursor, isTrue);
|
|
expect(query.realtime, isTrue);
|
|
expect(query.confidence, 'exact');
|
|
});
|
|
|
|
test('an if around both assignment and terminal is not conditional, and '
|
|
'a later assignment does not reach an earlier terminal', () {
|
|
final queries = byMember('platform');
|
|
expect(queries, hasLength(2));
|
|
final inside = queries.first;
|
|
final after = queries.last;
|
|
expect(ops(inside), ['userId ==', 'isActive ==']);
|
|
expect(inside.filters.every((f) => !f.conditional), isTrue);
|
|
expect(inside.orderBy, isEmpty);
|
|
expect(ops(after), ['userId ==', 'isActive ==']);
|
|
expect(after.filters.last.conditional, isTrue);
|
|
expect(after.orderBy.single.conditional, isFalse);
|
|
});
|
|
|
|
test('ternary alternatives are both recorded as conditional', () {
|
|
final query = single('ternaryClause');
|
|
expect(query.orderBy.map((o) => '${o.dir} ${o.conditional}'), [
|
|
'desc true',
|
|
'asc true',
|
|
]);
|
|
expect(query.filters.single.conditional, isFalse);
|
|
});
|
|
|
|
test('fields are followed through constructor and late initializers', () {
|
|
expect(
|
|
single('receipts', collection: 'notificationReceipts').origin,
|
|
'variable',
|
|
);
|
|
expect(single('receipts', collection: 'inbox').limit.value, 3);
|
|
});
|
|
|
|
test('a helper is inlined with its call-site arguments', () {
|
|
final query = single('listingPage');
|
|
expect(query.scope, 'collectionGroup');
|
|
expect(query.collection, 'products');
|
|
expect(ops(query), ['userId ==']);
|
|
expect(query.orderBy.single.dir, 'desc');
|
|
expect(query.limit.kind, 'variable');
|
|
expect(query.confidence, 'exact');
|
|
|
|
final stores = single('countActiveStores');
|
|
expect(stores.origin, 'helper');
|
|
expect(stores.aggregate, 'count');
|
|
expect(ops(stores), ['active ==']);
|
|
});
|
|
|
|
test('helpers are inlined one level deep only', () {
|
|
final query = single('nestedHelper');
|
|
expect(query.confidence, 'unknown');
|
|
expect(query.origin, 'unknown');
|
|
expect(
|
|
query.reasons,
|
|
contains('query helper nested more than one level deep'),
|
|
);
|
|
});
|
|
|
|
test('a query from a parameter is unknown but keeps its local clauses', () {
|
|
final query = single('fromParameter');
|
|
expect(query.confidence, 'unknown');
|
|
expect(query.origin, 'unknown');
|
|
expect(query.scope, isNull);
|
|
expect(query.collection, isNull);
|
|
expect(ops(query), ['visible ==']);
|
|
expect(query.limit.value, 10);
|
|
expect(query.reasons, contains('query arrives as a parameter'));
|
|
|
|
final fallback = single('fallback');
|
|
expect(fallback.confidence, 'unknown');
|
|
expect(ops(fallback), ['kind ==']);
|
|
expect(fallback.filters.single.conditional, isTrue);
|
|
});
|
|
|
|
test('inLoop covers loops and per-element closures, not a fixed list', () {
|
|
final loops = {
|
|
for (final query in byMember('inLoops')) query.collection: query.inLoop,
|
|
};
|
|
expect(loops, {
|
|
'items': true,
|
|
'products': true,
|
|
'audit': true,
|
|
'once': false,
|
|
});
|
|
expect(single('equalityAndOrder').inLoop, isFalse);
|
|
});
|
|
});
|
|
|
|
test('look-alike names outside cloud_firestore are never extracted', () {
|
|
// Iterable.where/take, a project class named Query/CollectionReference and
|
|
// a `collection()` on a non-Firestore class all share method names with
|
|
// the Firestore API. Receivers are matched by type and library, not name.
|
|
expect(
|
|
model.queries.where((q) => q.path == 'lib/negative_controls.dart'),
|
|
isEmpty,
|
|
);
|
|
expect(model.queries.where((q) => q.member == 'lookalikes'), isEmpty);
|
|
});
|
|
|
|
group('adapters', () {
|
|
List<QueryRecord> adapted(String member) => [
|
|
for (final query in byMember(member))
|
|
if (query.origin == 'adapter') query,
|
|
];
|
|
|
|
test('a collection group with an in filter', () {
|
|
final query = adapted('reconcile').single;
|
|
expect(query.scope, 'collectionGroup');
|
|
expect(query.collection, 'settlements');
|
|
expect(ops(query), ['status in']);
|
|
expect(query.filters.single.valueType, 'list');
|
|
expect(query.limit.value, 200);
|
|
expect(query.realtime, isFalse);
|
|
expect(query.confidence, 'exact');
|
|
});
|
|
|
|
test('equality lists, named range bounds and orderBy objects', () {
|
|
final query = adapted('digest').single;
|
|
expect(ops(query), [
|
|
'userId ==',
|
|
'isRead ==',
|
|
'createdAt >=',
|
|
'createdAt <',
|
|
]);
|
|
expect(query.filters[1].valueType, 'bool');
|
|
expect(query.filters[2].valueType, 'timestamp');
|
|
expect(query.orderBy.single.toJson(), {
|
|
'field': 'createdAt',
|
|
'dir': 'desc',
|
|
'conditional': false,
|
|
});
|
|
});
|
|
|
|
test('a local list built with add() is followed; if makes it '
|
|
'conditional', () {
|
|
final query = adapted('followers').single;
|
|
expect(ops(query), ['active ==', 'following array-contains']);
|
|
expect(query.filters.first.conditional, isTrue);
|
|
expect(query.filters.last.conditional, isFalse);
|
|
expect(query.orderBy.single.field, '__name__');
|
|
expect(query.cursor, isTrue);
|
|
expect(query.limit.value, 500);
|
|
expect(query.confidence, 'exact');
|
|
});
|
|
|
|
test('an enum operator argument maps through opMap', () {
|
|
final query = adapted('storeScoped').single;
|
|
expect(query.pathPattern, 'stores/*/payouts');
|
|
expect(ops(query), ['amount >']);
|
|
expect(query.filters.single.valueType, 'int');
|
|
});
|
|
|
|
test('a transaction method on a second interface is matched', () {
|
|
final query = adapted('confirm').single;
|
|
expect(query.collection, 'confirmations');
|
|
expect(ops(query), ['token ==']);
|
|
expect(query.limit.value, 1);
|
|
});
|
|
|
|
test('calls through an implementing class match, in a loop', () {
|
|
final query = adapted('concrete').single;
|
|
expect(query.collection, 'carts');
|
|
expect(query.inLoop, isTrue);
|
|
});
|
|
|
|
test('an unmapped method is not a query', () {
|
|
expect(byMember('notAQuery'), isEmpty);
|
|
});
|
|
|
|
test('invalid configs are refused with a clear message', () {
|
|
expect(() => parseAdapterConfig('{}'), throwsA(isA<PipeException>()));
|
|
expect(
|
|
() => parseAdapterConfig(
|
|
'{"adapters":[{"interface":"G","library":"package:a/a.dart",'
|
|
'"methods":{"q":{"scope":"table"}}}]}',
|
|
),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(e) => e.toString(),
|
|
'message',
|
|
contains('scope'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() => parseAdapterConfig(
|
|
'{"adapters":[{"interface":"G","library":"package:a/a.dart",'
|
|
'"methods":{"q":{"scope":"collection"}},'
|
|
'"types":{"W":{"field":0,"op":"LIKE"}}}]}',
|
|
),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(e) => e.toString(),
|
|
'message',
|
|
contains('LIKE'),
|
|
),
|
|
),
|
|
);
|
|
// YAML is accepted too.
|
|
final yaml = parseAdapterConfig('''
|
|
adapters:
|
|
- interface: G
|
|
library: package:a/a.dart
|
|
methods:
|
|
q: {scope: collectionGroup, path: 0}
|
|
''');
|
|
expect(yaml.single.methods['q']!.scope, 'collectionGroup');
|
|
});
|
|
});
|
|
|
|
test(
|
|
'the shared pipeline carries the inventory alongside the findings',
|
|
() async {
|
|
final analysis = await analyzeProject(project, concurrency: 1);
|
|
try {
|
|
expect(analysis.firestore.queries, isNotEmpty);
|
|
// No adapters were passed, so no adapter records.
|
|
expect(
|
|
analysis.firestore.queries.where((q) => q.origin == 'adapter'),
|
|
isEmpty,
|
|
);
|
|
} finally {
|
|
await analysis.dispose();
|
|
}
|
|
},
|
|
);
|
|
|
|
test('queries are sorted by site', () {
|
|
final sites = [for (final query in model.queries) (query.path, query.line)];
|
|
final sorted = [...sites]
|
|
..sort((a, b) {
|
|
final byPath = a.$1.compareTo(b.$1);
|
|
return byPath != 0 ? byPath : a.$2.compareTo(b.$2);
|
|
});
|
|
expect(sites, sorted);
|
|
});
|
|
|
|
test('the report root never carries an absolute path', () {
|
|
expect(queriesRoot('/work/app', cwd: '/work'), 'app');
|
|
expect(queriesRoot('/work', cwd: '/work'), '.');
|
|
expect(queriesRoot('/elsewhere/app', cwd: '/work'), 'app');
|
|
});
|
|
|
|
group('pipe queries CLI', () {
|
|
Future<ProcessResult> pipe(List<String> arguments) => Process.run(
|
|
Platform.resolvedExecutable,
|
|
['bin/pipe.dart', 'queries', ...arguments, '-c', '1'],
|
|
);
|
|
|
|
test('--format json keeps stdout pure JSON; notes go to stderr', () async {
|
|
final result = await pipe([
|
|
fixturePath,
|
|
'--format',
|
|
'json',
|
|
'--adapter-config',
|
|
adapterConfigPath,
|
|
]);
|
|
expect(result.exitCode, 0, reason: '${result.stderr}');
|
|
final json = jsonDecode(result.stdout as String) as Map<String, Object?>;
|
|
expect(json['version'], 1);
|
|
expect(json['root'], fixturePath);
|
|
expect((json['coverage'] as Map)['complete'], isTrue);
|
|
final totals = json['totals'] as Map<String, Object?>;
|
|
final queries = json['queries'] as List<Object?>;
|
|
expect(totals['queries'], queries.length);
|
|
expect(totals['byConfidence'], isA<Map<String, Object?>>());
|
|
expect((totals['byOrigin'] as Map)['adapter'], 6);
|
|
expect(json['unresolvedFirestoreUris'], isEmpty);
|
|
expect(result.stderr as String, contains('document read'));
|
|
});
|
|
|
|
test('--format md groups queries by collection', () async {
|
|
final result = await pipe([fixturePath, '--format', 'md']);
|
|
expect(result.exitCode, 0, reason: '${result.stderr}');
|
|
final text = result.stdout as String;
|
|
expect(text, startsWith('# Firestore queries'));
|
|
expect(text, contains('Coverage: '));
|
|
expect(text, contains('## `notifications` ('));
|
|
expect(text, contains('## `(unresolved)` ('));
|
|
expect(text, isNot(contains('ignore previous')));
|
|
// Unresolved queries are listed last.
|
|
expect(
|
|
text.indexOf('## `(unresolved)`'),
|
|
greaterThan(text.indexOf('## `userSubscriptions`')),
|
|
);
|
|
});
|
|
|
|
test('a project without cloud_firestore says so, and '
|
|
'--require-firestore fails', () async {
|
|
final plain = await pipe(['test/fixtures/stuck_loading']);
|
|
expect(plain.exitCode, 0, reason: '${plain.stderr}');
|
|
final json = jsonDecode(plain.stdout as String) as Map<String, Object?>;
|
|
expect(json['queries'], isEmpty);
|
|
expect(
|
|
(json['notes'] as List).join(),
|
|
contains('cloud_firestore is not in the package graph'),
|
|
);
|
|
|
|
final strict = await pipe([
|
|
'test/fixtures/stuck_loading',
|
|
'--require-firestore',
|
|
]);
|
|
expect(strict.exitCode, 2);
|
|
expect(strict.stderr as String, contains('--require-firestore'));
|
|
});
|
|
|
|
test('a directory inside the package is a scope, not a project', () async {
|
|
// `pipe queries lib` looked for lib/.dart_tool/package_config.json and
|
|
// failed. The graph is the package's; the path only narrows the scan.
|
|
final result = await pipe(['$fixturePath/lib/data']);
|
|
expect(result.exitCode, 0, reason: '${result.stderr}');
|
|
final json = jsonDecode(result.stdout as String) as Map<String, Object?>;
|
|
expect(json['root'], fixturePath);
|
|
final coverage = json['coverage'] as Map<String, Object?>;
|
|
expect(coverage['discovered'], 1);
|
|
expect(coverage['complete'], isTrue);
|
|
// lib/data holds only the gateway, so the queries the rest of lib issues
|
|
// are out of scope.
|
|
final sites = [
|
|
for (final query in json['queries'] as List<Object?>)
|
|
(query as Map<String, Object?>)['site'] as String,
|
|
];
|
|
expect(sites, everyElement(startsWith('lib/data/')));
|
|
expect(model.queries, isNotEmpty);
|
|
expect(json['unresolvedFirestoreUris'], isEmpty);
|
|
});
|
|
|
|
test('--dir under a scoped path is relative to that path', () async {
|
|
final result = await pipe(['$fixturePath/lib', '--dir', 'data']);
|
|
expect(result.exitCode, 0, reason: '${result.stderr}');
|
|
final json = jsonDecode(result.stdout as String) as Map<String, Object?>;
|
|
expect((json['coverage'] as Map)['discovered'], 1);
|
|
});
|
|
|
|
test('an unresolved package is reported at its root, not the scope', () {
|
|
final copy = Directory.systemTemp.createTempSync('pipe-queries-fresh');
|
|
try {
|
|
File('${copy.path}/pubspec.yaml').writeAsStringSync('name: fresh\n');
|
|
File('${copy.path}/lib/fresh.dart')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('void main() {}\n');
|
|
final root = copy.resolveSymbolicLinksSync();
|
|
expect(
|
|
() => PipeRunner().run(['queries', '$root/lib']),
|
|
throwsA(
|
|
isA<PipeException>()
|
|
.having((e) => e.message, 'message', contains('package graph'))
|
|
.having(
|
|
(e) => e.remedy,
|
|
'remedy',
|
|
allOf(
|
|
endsWith('pub get` in $root'),
|
|
isNot(contains('$root/lib')),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
} finally {
|
|
copy.deleteSync(recursive: true);
|
|
}
|
|
});
|
|
});
|
|
}
|