diff --git a/app/lib/fake/backend/fake_pub_worker.dart b/app/lib/fake/backend/fake_pub_worker.dart index f1ab860338..a03cdb4d89 100644 --- a/app/lib/fake/backend/fake_pub_worker.dart +++ b/app/lib/fake/backend/fake_pub_worker.dart @@ -9,7 +9,9 @@ import 'dart:math'; import 'package:_pub_shared/dartdoc/dartdoc_page.dart'; import 'package:_pub_shared/data/package_api.dart'; import 'package:_pub_shared/data/task_payload.dart'; +import 'package:_pub_shared/worker/docker_utils.dart'; import 'package:clock/clock.dart'; +import 'package:gcloud/service_scope.dart'; import 'package:http/http.dart'; import 'package:http_parser/http_parser.dart'; import 'package:indexed_blob/indexed_blob.dart'; @@ -18,6 +20,7 @@ import 'package:pana/pana.dart'; import 'package:path/path.dart' as p; import 'package:pub_dev/fake/backend/fake_pana_runner.dart'; import 'package:pub_dev/frontend/handlers/pubapi.client.dart'; +import 'package:pub_dev/frontend/static_files.dart'; import 'package:pub_dev/scorecard/backend.dart'; import 'package:pub_dev/service/async_queue/async_queue.dart'; import 'package:pub_dev/shared/utils.dart'; @@ -65,13 +68,31 @@ Future processTasksLocallyWithPubWorker() async { ]); } +/// Process analysis tasks locally, using either: +/// - `fake`: the semi-randomized fake analysis that is fast +/// - `local`: running pkg/pub_worker using the same context as the app +/// - `worker`: running pkg/pub_worker using the dockerized container +Future processTaskFakeLocalOrWorker(String analysis) async { + if (analysis == 'none') { + return; + } else if (analysis == 'local') { + await _analyzeLocal(); + } else if (analysis == 'worker') { + await _analyzeWorker(); + } else if (analysis == 'fake') { + await processTasksWithFakePanaAndDartdoc(); + } else { + throw ArgumentError('Unknown analysis: `$analysis`'); + } +} + /// Updates the task status for all packages and imitates /// pub_worker using fake pana and dartdoc results. Future processTasksWithFakePanaAndDartdoc() async { - await taskBackend.backfillAndProcessAllPackages(_processPayload); + await taskBackend.backfillAndProcessAllPackages(_fakeAnalysis); } -Future _processPayload(Payload payload) async { +Future _fakeAnalysis(Payload payload) async { for (final v in payload.versions) { final client = httpClientWithAuthorization( tokenProvider: () async => v.token, @@ -154,10 +175,40 @@ Future _processPayload(Payload payload) async { } } +Future _analyzeLocal() async { + await fork(() async { + await taskBackend.backfillAndProcessAllPackages((Payload payload) async { + final arguments = [json.encode(payload.toJson())]; + final pr = await Process.run( + Platform.resolvedExecutable, + ['run', 'pub_worker', ...arguments], + workingDirectory: p.join(resolveAppDir(), '..', 'pkg', 'pub_worker'), + ); + if (pr.exitCode != 0) { + throw Exception('Unexpected status code: ${pr.exitCode} ${pr.stdout}'); + } + }); + }); +} + +Future _analyzeWorker() async { + await buildDockerImage(); + await fork(() async { + await taskBackend.backfillAndProcessAllPackages((Payload payload) async { + final p = await startDockerAnalysis(payload); + final exitCode = await p.exitCode; + if (exitCode != 0) { + throw Exception( + 'Failed to analyze ${payload.package} with exitCode $exitCode'); + } + }); + }); +} + Future fakeCloudComputeInstanceRunner(FakeCloudInstance instance) async { final payload = Payload.fromJson( json.decode(instance.arguments.first) as Map); - await _processPayload(payload); + await _fakeAnalysis(payload); } Map _fakeDartdocFiles( diff --git a/app/lib/fake/server/fake_server_entrypoint.dart b/app/lib/fake/server/fake_server_entrypoint.dart index 88b75d6ef9..ccec27de98 100644 --- a/app/lib/fake/server/fake_server_entrypoint.dart +++ b/app/lib/fake/server/fake_server_entrypoint.dart @@ -176,7 +176,8 @@ Future _testProfile(shelf.Request rq) async { profile: profile, source: ImportSource.autoGenerated(), ); - await processTasksWithFakePanaAndDartdoc(); + final analysis = (map['analysis'] as String?) ?? 'fake'; + await processTaskFakeLocalOrWorker(analysis); return shelf.Response.ok('{}'); } diff --git a/app/lib/fake/tool/init_data_file.dart b/app/lib/fake/tool/init_data_file.dart index 34b7c8ca9b..04f8a684d2 100644 --- a/app/lib/fake/tool/init_data_file.dart +++ b/app/lib/fake/tool/init_data_file.dart @@ -2,20 +2,15 @@ // 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:convert'; import 'dart:io'; -import 'package:_pub_shared/data/task_payload.dart'; -import 'package:_pub_shared/worker/docker_utils.dart'; import 'package:args/command_runner.dart'; -import 'package:gcloud/service_scope.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:pub_dev/fake/backend/fake_pub_worker.dart'; import 'package:pub_dev/frontend/static_files.dart'; import 'package:pub_dev/service/services.dart'; -import 'package:pub_dev/task/backend.dart'; import 'package:pub_dev/tool/test_profile/import_source.dart'; import 'package:pub_dev/tool/test_profile/importer.dart'; import 'package:pub_dev/tool/test_profile/models.dart'; @@ -90,44 +85,8 @@ class FakeInitDataFileCommand extends Command { : ImportSource.autoGenerated(), ); - if (analysis == 'local') { - await _analyzeLocal(); - } else if (analysis == 'worker') { - await _analyzeWorker(); - } else if (analysis == 'fake') { - await processTasksWithFakePanaAndDartdoc(); - } + await processTaskFakeLocalOrWorker(analysis); }); await state.save(dataFile); } } - -Future _analyzeLocal() async { - await fork(() async { - await taskBackend.backfillAndProcessAllPackages((Payload payload) async { - final arguments = [json.encode(payload.toJson())]; - final pr = await Process.run( - Platform.resolvedExecutable, - ['run', 'pub_worker', ...arguments], - workingDirectory: p.join(resolveAppDir(), '..', 'pkg', 'pub_worker'), - ); - if (pr.exitCode != 0) { - throw Exception('Unexpected status code: ${pr.exitCode} ${pr.stdout}'); - } - }); - }); -} - -Future _analyzeWorker() async { - await buildDockerImage(); - await fork(() async { - await taskBackend.backfillAndProcessAllPackages((Payload payload) async { - final p = await startDockerAnalysis(payload); - final exitCode = await p.exitCode; - if (exitCode != 0) { - throw Exception( - 'Failed to analyze ${payload.package} with exitCode $exitCode'); - } - }); - }); -} diff --git a/pkg/pub_integration/test/search_completition_test.dart b/pkg/pub_integration/test/search_completition_test.dart new file mode 100644 index 0000000000..645c2682b3 --- /dev/null +++ b/pkg/pub_integration/test/search_completition_test.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2024, 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:convert'; + +import 'package:http/http.dart' as http; +import 'package:pub_integration/src/fake_test_context_provider.dart'; +import 'package:pub_integration/src/test_browser.dart'; +import 'package:puppeteer/puppeteer.dart'; +import 'package:test/test.dart'; + +void main() { + group('search completition', () { + late final TestContextProvider fakeTestScenario; + final httpClient = http.Client(); + + setUpAll(() async { + fakeTestScenario = await TestContextProvider.start(); + }); + + tearDownAll(() async { + await fakeTestScenario.close(); + httpClient.close(); + }); + + test('bulk tests', () async { + final origin = fakeTestScenario.pubHostedUrl; + // init server data + // + // The test profile import uses a fake analysis by default, which + // assigns tags with a pseudorandom process (based on the hash of the + // package's name and sometimes the version), with a few hardcoded + // patterns, e.g. `flutter_*` packages will get `sdk:flutter` tag assigned. + // + // This imports 100 packages with these semi-random tags, and adding and + // removing filters works because of the number of packages and their + // tags are kind of random. + await httpClient.post(Uri.parse('$origin/fake-test-profile'), + body: json.encode({ + 'testProfile': { + 'defaultUser': 'admin@pub.dev', + 'packages': [ + { + 'name': 'oxygen', + 'publisher': 'example.com', + }, + ], + }, + 'analysis': 'local', + })); + + final user = await fakeTestScenario.createAnonymousTestUser(); + + await user.withBrowserPage((page) async { + await page.gotoOrigin('/experimental?search-completion=1'); + + await page.gotoOrigin('/'); + await page.keyboard.type('is:un'); + await Future.delayed(Duration(milliseconds: 200)); + await page.keyboard.press(Key.enter); + await Future.delayed(Duration(milliseconds: 200)); + await page.keyboard.press(Key.enter); + await page.waitForNavigation(); + + // TODO: try to fix form submission to trim whitespaces + expect(page.url, '$origin/packages?q=is%3Aunlisted+'); + }); + + await user.withBrowserPage((page) async { + await page.gotoOrigin('/experimental?search-completion=1'); + + await page.gotoOrigin('/packages?q=abc'); + await page.focus('input[name="q"]'); + // go to the end of the input field and start typing + await page.keyboard.press(Key.arrowDown); + await page.keyboard.type(' -sdk:fl'); + await Future.delayed(Duration(milliseconds: 200)); + await page.keyboard.press(Key.enter); + await Future.delayed(Duration(milliseconds: 200)); + await page.keyboard.press(Key.enter); + await page.waitForNavigation(); + + // TODO: try to fix form submission to trim whitespaces + expect(page.url, '$origin/packages?q=abc+-sdk%3Aflutter+'); + }); + }); + }, timeout: Timeout.factor(testTimeoutFactor)); +}