blob: eafddd18b4be1f87a34c53f5b65683ba0fa3dc5a [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/autofill/ios/browser/form_fetch_batcher.h"
#import <memory>
#import "base/functional/bind.h"
#import "base/location.h"
#import "base/task/sequenced_task_runner.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/task_environment.h"
#import "base/time/time.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/ios/browser/autofill_driver_ios_bridge.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "url/gurl.h"
namespace {
constexpr base::TimeDelta kBatchPeriodMs = base::Milliseconds(100);
// Generic form fetch completion callback that flips a bool to true when called.
void FormFetchCompletionCallback(
bool* complete_ptr,
std::optional<std::vector<autofill::FormData>> result) {
*complete_ptr = true;
}
autofill::FormData MakeTestFormData(const std::u16string& name) {
autofill::FormData form_data;
form_data.set_name(name);
return form_data;
}
} // namespace
namespace autofill {
class Section;
}
// AutofillDriverIosBridge used for testing. Provides a simple implementation of
// the methods that are used during testing, e.g. call the completion block upon
// calling -fetchFormsFiltered.
@interface TestAutofillDriverIOSBridge : NSObject <AutofillDriverIOSBridge>
// YES when the bridge does the form fetch asynchronously. Use RunUntilIdle() to
// run the async task.
@property(nonatomic, assign) BOOL async;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithForms:(std::vector<autofill::FormData>)forms;
@end
@implementation TestAutofillDriverIOSBridge {
std::vector<autofill::FormData> _forms;
}
- (instancetype)initWithForms:(std::vector<autofill::FormData>)forms {
if ((self = [super init])) {
_forms = std::move(forms);
}
return self;
}
- (void)fillData:(const std::vector<autofill::FormFieldData::FillData>&)fields
section:(const autofill::Section&)section
inFrame:(web::WebFrame*)frame {
}
- (void)fillSpecificFormField:(const autofill::FieldRendererId&)field
withValue:(const std::u16string)value
inFrame:(web::WebFrame*)frame {
}
- (void)handleParsedForms:
(const std::vector<
raw_ptr<autofill::FormStructure, VectorExperimental>>&)forms
inFrame:(web::WebFrame*)frame {
}
- (void)fillFormDataPredictions:
(const std::vector<autofill::FormDataPredictions>&)forms
inFrame:(web::WebFrame*)frame {
}
- (void)scanFormsInWebState:(web::WebState*)webState
inFrame:(web::WebFrame*)webFrame {
}
- (void)notifyFormsSeen:(const std::vector<autofill::FormData>&)updatedForms
inFrame:(web::WebFrame*)frame {
}
- (void)fetchFormsFiltered:(BOOL)filtered
withName:(const std::u16string&)formName
inFrame:(web::WebFrame*)frame
completionHandler:(FormFetchCompletion)completionHandler {
if (self.async) {
auto asyncTask = base::BindOnce(
[](FormFetchCompletion completion,
std::vector<autofill::FormData>* result) {
std::move(completion).Run(*result);
},
std::move(completionHandler), &_forms);
// Push a task in the sequence.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(asyncTask));
} else {
std::move(completionHandler).Run(_forms);
}
}
@end
class FormFetchBatcherTest : public PlatformTest {
protected:
FormFetchBatcherTest()
: test_bridge_([[TestAutofillDriverIOSBridge alloc]
initWithForms:{MakeTestFormData(u"form1"),
MakeTestFormData(u"form2")}]),
fake_web_frame_(web::FakeWebFrame::Create("main_frame_id", true)),
batcher_(test_bridge_,
fake_web_frame_.get()->AsWeakPtr(),
kBatchPeriodMs) {}
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
TestAutofillDriverIOSBridge* test_bridge_;
std::unique_ptr<web::WebFrame> fake_web_frame_;
autofill::FormFetchBatcher batcher_;
base::HistogramTester histogram_tester_;
};
// Tests that the requests pushed to the scheduled batch are indeed completed
// once the batch is done. Tests with a batch of 2 requests, namely r1 and r2.
TEST_F(FormFetchBatcherTest, Batch) {
// Completion trackers, true when the request is completed.
bool r1_completed = false;
bool r2_completed = false;
// Verify that there is not any scheduled batch at this point, not until the
// first request push.
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Push request #1 (r1).
{
batcher_.PushRequest(
base::BindOnce(&FormFetchCompletionCallback, &r1_completed));
}
// Verify that the batch was scheduled by the first push.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Advance time but not enough to hit the schedule.
base::TimeDelta remaining_time = base::Milliseconds(50);
task_environment_.AdvanceClock(kBatchPeriodMs - remaining_time);
EXPECT_FALSE(r1_completed);
EXPECT_FALSE(r2_completed);
// Verif that the batch is still scheduled as the delay wasn't reached.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Push request #2 (r2).
{
batcher_.PushRequest(
base::BindOnce(&FormFetchCompletionCallback, &r2_completed));
}
// Verify that the new request was included in the same batch as r1, where
// there is still only one pending batch but now with 2 requests in it.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Run the scheduled batch. Advance the time so the scheduled task is run.
task_environment_.FastForwardBy(remaining_time + base::Milliseconds(10));
// Verify that the batch is done and no other batch was rescheduled since
// there are no more requests in the queue.
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Verify that the batch of requests was completed by the batcher.
EXPECT_TRUE(r1_completed);
EXPECT_TRUE(r2_completed);
histogram_tester_.ExpectUniqueSample(
"Autofill.iOS.FormExtraction.ForScan.BatchSize",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
// Tests that once a batch is done, another one can be scheduled.
TEST_F(FormFetchBatcherTest, Batch_Reschedule) {
// Schedule an initial batch with request #1 in it (r1).
// Completion tracker, true when the request is completed.
auto r1_completed = std::make_unique<bool>(false);
// Push request #1 (r1).
{
batcher_.PushRequest(
base::BindOnce(&FormFetchCompletionCallback, r1_completed.get()));
}
// Advance time enough to trigger the first batch.
task_environment_.FastForwardBy(kBatchPeriodMs + base::Milliseconds(50));
EXPECT_TRUE(*r1_completed);
*r1_completed = false;
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Schedule a new batch with request 2 in it (r2).
// Completion trackers, true when the request is completed.
auto r2_completed = std::make_unique<bool>(false);
// Push request #2 (r2).
{
batcher_.PushRequest(
base::BindOnce(&FormFetchCompletionCallback, r2_completed.get()));
}
// Verify that a new batch was scheduled.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Advance time enough to trigger the first batch.
task_environment_.FastForwardBy(kBatchPeriodMs + base::Milliseconds(50));
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Verify that the batch of requests was completed by the batcher.
EXPECT_TRUE(*r2_completed);
// As request #1 was already completed, it should not had been part of the
// second batch.
EXPECT_FALSE(*r1_completed);
// Verify that each batch was recorded.
histogram_tester_.ExpectUniqueSample(
"Autofill.iOS.FormExtraction.ForScan.BatchSize",
/*sample=*/1,
/*expected_bucket_count=*/2);
}
// Tests that a batch isn't scheduled if not needed (i.e. there are requests to
// be completed).
TEST_F(FormFetchBatcherTest, Batch_OnlyWhenNeeded) {
// Advance time enough to trigger the first batch if there was one needed.
task_environment_.AdvanceClock(kBatchPeriodMs + base::Milliseconds(50));
// Verify that no batch was scheduled.
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
histogram_tester_.ExpectTotalCount(
"Autofill.iOS.FormExtraction.ForScan.BatchSize",
/*exprected_count=*/0);
}
// Tests that the pending batch task is canceled and the batch is run
// immediately when PushRequestAndRun() is used.
TEST_F(FormFetchBatcherTest, Batch_PushAndRun) {
// Completion trackers, true when the request is completed.
bool r1_completed = false;
bool r2_completed = false;
// Verify that there is not any scheduled batch at this point, not until the
// first request push.
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Push request #1 (r1).
{
batcher_.PushRequest(
base::BindOnce(&FormFetchCompletionCallback, &r1_completed));
}
// Verify that the batch was scheduled by the first push.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
ASSERT_FALSE(r1_completed);
ASSERT_FALSE(r2_completed);
// Push request #2 (r2) and run it immediately.
{
batcher_.PushRequestAndRun(
base::BindOnce(&FormFetchCompletionCallback, &r2_completed));
}
// Verify that the scheduled batch was canceled.
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Verify that the batch of requests was completed by the batcher.
EXPECT_TRUE(r1_completed);
EXPECT_TRUE(r2_completed);
histogram_tester_.ExpectUniqueSample(
"Autofill.iOS.FormExtraction.ForScan.BatchSize",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
// Tests that tasks aren't scheduled as along as the form data isn't received
// for PushAndRun().
TEST_F(FormFetchBatcherTest, Batch_PushAndRun_AndRunAgain) {
// Completion trackers, true when the request is completed.
bool r1_completed = false;
bool r2_completed = false;
// Verify that there is not any scheduled batch at this point, not until the
// first request push.
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Switch the bridge to async so the fetch request for PushRequestAndRun()
// isn't run immediately.
test_bridge_.async = YES;
// Push request #1 (r1) and run it immediately.
{
batcher_.PushRequestAndRun(
base::BindOnce(&FormFetchCompletionCallback, &r1_completed));
}
// Verify that the fetch task was pushed in the sequence instead of run
// immediately.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Push request #2 (r2).
{
batcher_.PushRequest(
base::BindOnce(&FormFetchCompletionCallback, &r2_completed));
}
// Verify that there is still only one task pending as PushRequest() should
// not start another task.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Run the task that was pushed.
task_environment_.RunUntilIdle();
// Both requests should have been batched together and run.
EXPECT_TRUE(r1_completed);
EXPECT_TRUE(r2_completed);
// Verify that both fetch requests are counted even if only one batching task
// was used.
histogram_tester_.ExpectUniqueSample(
"Autofill.iOS.FormExtraction.ForScan.BatchSize",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
// Tests that the new requests that are added while completing the current
// batch of requests are pushed for later into the next batch. This test also
// ensures that doing that doesn't cause memory issues like what we've seen in
// http://crbug.com/379087890.
TEST_F(FormFetchBatcherTest, Batch_Completion_NewRequests) {
// Schedule an initial batch with request #1 in it (r1).
// Completion tracker, true when the request is completed.
bool r1_completed = false;
bool r2_completed = false;
// Push request and run request #1 (r1). This request upon completion will
// immediately push another request.
{
batcher_.PushRequest(base::BindOnce(
[](autofill::FormFetchBatcher* batcher,
FormFetchCompletion other_completion, bool* r1_completed,
std::optional<std::vector<autofill::FormData>> result) {
*r1_completed = true;
batcher->PushRequest(std::move(other_completion));
},
&batcher_, base::BindOnce(&FormFetchCompletionCallback, &r2_completed),
&r1_completed));
}
task_environment_.FastForwardBy(kBatchPeriodMs + base::Milliseconds(50));
EXPECT_TRUE(r1_completed);
r1_completed = false;
// Verify that that request pushed by request #1 was enqueued for the next
// batch.
ASSERT_EQ(1u, task_environment_.GetPendingMainThreadTaskCount());
// Advance time enough to trigger the following batch.
task_environment_.FastForwardBy(kBatchPeriodMs + base::Milliseconds(50));
ASSERT_EQ(0u, task_environment_.GetPendingMainThreadTaskCount());
// Verify that the batch of requests was completed by the batcher.
EXPECT_TRUE(r2_completed);
// As request #1 was already completed, it should not had been part of the
// second batch.
EXPECT_FALSE(r1_completed);
// Verify that each batch was recorded.
histogram_tester_.ExpectUniqueSample(
"Autofill.iOS.FormExtraction.ForScan.BatchSize",
/*sample=*/1,
/*expected_bucket_count=*/2);
}
// Tests fetch filtered requests.
TEST_F(FormFetchBatcherTest, Filtered) {
// Hold the fetched forms for each request.
std::vector<autofill::FormData> r1_forms;
std::vector<autofill::FormData> r2_forms;
auto callback = [](std::vector<autofill::FormData>* captured_forms,
std::optional<std::vector<autofill::FormData>> result) {
CHECK(result);
*captured_forms = *result;
};
// Push request #1 (r1).
{ batcher_.PushRequest(base::BindOnce(callback, &r1_forms), u"form1"); }
// Push request #2 (r2).
{ batcher_.PushRequest(base::BindOnce(callback, &r2_forms), u"form2"); }
task_environment_.FastForwardBy(kBatchPeriodMs + base::Milliseconds(50));
// Verify that only the forms matching the name specified in the request are
// returned for each request.
EXPECT_THAT(r1_forms, testing::ElementsAre(testing::Property(
&autofill::FormData::name, u"form1")));
EXPECT_THAT(r2_forms, testing::ElementsAre(testing::Property(
&autofill::FormData::name, u"form2")));
}