| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <limits> |
| |
| #include "base/base_paths.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/path_service.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "content/public/browser/service_process_host.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "services/data_decoder/public/cpp/decode_image.h" |
| #include "services/data_decoder/public/mojom/data_decoder_service.mojom.h" |
| #include "services/data_decoder/public/mojom/image_decoder.mojom.h" |
| #include "services/data_decoder/public/mojom/json_parser.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| using ::testing::Pair; |
| using ::testing::UnorderedElementsAre; |
| |
| namespace content { |
| |
| namespace { |
| |
| // Populates `output` and returns true on success (i.e. if `relative_path` |
| // exists and can be read into `output`). Otherwise returns false. |
| bool ReadTestFile(const base::FilePath& relative_path, |
| std::vector<uint8_t>& output) { |
| base::FilePath source_root_dir; |
| if (!base::PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir)) |
| return false; |
| |
| std::string file_contents_as_string; |
| { |
| base::ScopedAllowBlockingForTesting allow_file_io_for_testing; |
| base::FilePath absolute_path = source_root_dir.Append(relative_path); |
| if (!base::ReadFileToString(absolute_path, &file_contents_as_string)) |
| return false; |
| } |
| |
| // Convert chars to uint8_ts. |
| for (const char& c : file_contents_as_string) |
| output.push_back(c); |
| |
| return true; |
| } |
| |
| // Populates `out_measurement_value` and returns true on success (i.e. if the |
| // `metric_name` has a single measurement in `histograms`). Otherwise returns |
| // false. |
| bool GetSingleMeasurement(const base::HistogramTester& histograms, |
| const char* metric_name, |
| base::TimeDelta& out_measurement_value) { |
| DCHECK(metric_name); |
| |
| std::vector<base::Bucket> buckets = histograms.GetAllSamples(metric_name); |
| if (buckets.size() != 1u) |
| return false; |
| |
| EXPECT_EQ(1u, buckets.size()); |
| out_measurement_value = base::Milliseconds(buckets.front().min); |
| return true; |
| } |
| |
| } // namespace |
| |
| using DataDecoderBrowserTest = ContentBrowserTest; |
| |
| class ServiceProcessObserver : public ServiceProcessHost::Observer { |
| public: |
| ServiceProcessObserver() { ServiceProcessHost::AddObserver(this); } |
| |
| ServiceProcessObserver(const ServiceProcessObserver&) = delete; |
| ServiceProcessObserver& operator=(const ServiceProcessObserver&) = delete; |
| |
| ~ServiceProcessObserver() override { |
| ServiceProcessHost::RemoveObserver(this); |
| } |
| |
| int instances_started() const { return instances_started_; } |
| |
| void WaitForNextLaunch() { |
| launch_wait_loop_.emplace(); |
| launch_wait_loop_->Run(); |
| } |
| |
| void OnServiceProcessLaunched(const ServiceProcessInfo& info) override { |
| if (info.IsService<data_decoder::mojom::DataDecoderService>()) { |
| ++instances_started_; |
| if (launch_wait_loop_) |
| launch_wait_loop_->Quit(); |
| } |
| } |
| |
| private: |
| absl::optional<base::RunLoop> launch_wait_loop_; |
| int instances_started_ = 0; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, Launch) { |
| ServiceProcessObserver observer; |
| |
| // Verifies that the DataDecoder client object launches a service process as |
| // needed. |
| data_decoder::DataDecoder decoder; |
| |
| // |GetService()| must always ensure a connection to the service on all |
| // platforms, so we use it instead of a more specific API whose behavior may |
| // vary across platforms. |
| decoder.GetService(); |
| |
| observer.WaitForNextLaunch(); |
| EXPECT_EQ(1, observer.instances_started()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, LaunchIsolated) { |
| ServiceProcessObserver observer; |
| |
| // Verifies that separate DataDecoder client objects will launch separate |
| // service processes. We also bind a JsonParser interface to ensure that the |
| // instances don't go idle. |
| data_decoder::DataDecoder decoder1; |
| mojo::Remote<data_decoder::mojom::JsonParser> parser1; |
| decoder1.GetService()->BindJsonParser(parser1.BindNewPipeAndPassReceiver()); |
| observer.WaitForNextLaunch(); |
| EXPECT_EQ(1, observer.instances_started()); |
| |
| data_decoder::DataDecoder decoder2; |
| mojo::Remote<data_decoder::mojom::JsonParser> parser2; |
| decoder2.GetService()->BindJsonParser(parser2.BindNewPipeAndPassReceiver()); |
| observer.WaitForNextLaunch(); |
| EXPECT_EQ(2, observer.instances_started()); |
| |
| // Both interfaces should be connected end-to-end. |
| parser1.FlushForTesting(); |
| parser2.FlushForTesting(); |
| EXPECT_TRUE(parser1.is_connected()); |
| EXPECT_TRUE(parser2.is_connected()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, DecodeImageIsolated) { |
| std::vector<uint8_t> file_contents; |
| base::FilePath content_test_data_path = GetTestDataFilePath(); |
| base::FilePath png_path = |
| content_test_data_path.AppendASCII("site_isolation/png-corp.png"); |
| ASSERT_TRUE(ReadTestFile(png_path, file_contents)); |
| |
| base::HistogramTester histograms; |
| { |
| base::RunLoop run_loop; |
| data_decoder::DecodeImageCallback callback = |
| base::BindLambdaForTesting([&](const SkBitmap& decoded_bitmap) { |
| EXPECT_EQ(100, decoded_bitmap.width()); |
| EXPECT_EQ(100, decoded_bitmap.height()); |
| run_loop.Quit(); |
| }); |
| data_decoder::DecodeImageIsolated( |
| file_contents, data_decoder::mojom::ImageCodec::kDefault, |
| false, // shrink_to_fit |
| std::numeric_limits<uint32_t>::max(), // max_size_in_bytes |
| gfx::Size(), // desired_image_frame_size |
| std::move(callback)); |
| run_loop.Run(); |
| } |
| |
| FetchHistogramsFromChildProcesses(); |
| EXPECT_THAT( |
| histograms.GetTotalCountsForPrefix("Security.DataDecoder"), |
| UnorderedElementsAre( |
| Pair("Security.DataDecoder.Image.Isolated.EndToEndTime", 1), |
| Pair("Security.DataDecoder.Image.Isolated.ProcessOverhead", 1), |
| Pair("Security.DataDecoder.Image.DecodingTime", 1))); |
| |
| base::TimeDelta end_to_end_duration_estimate; |
| EXPECT_TRUE(GetSingleMeasurement( |
| histograms, "Security.DataDecoder.Image.Isolated.EndToEndTime", |
| end_to_end_duration_estimate)); |
| |
| base::TimeDelta overhead_estimate; |
| EXPECT_TRUE(GetSingleMeasurement( |
| histograms, "Security.DataDecoder.Image.Isolated.ProcessOverhead", |
| overhead_estimate)); |
| |
| base::TimeDelta decoding_duration_estimate; |
| EXPECT_TRUE(GetSingleMeasurement(histograms, |
| "Security.DataDecoder.Image.DecodingTime", |
| decoding_duration_estimate)); |
| |
| EXPECT_LE(decoding_duration_estimate, end_to_end_duration_estimate); |
| EXPECT_LE(overhead_estimate, end_to_end_duration_estimate); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, DecodeImage) { |
| std::vector<uint8_t> file_contents; |
| base::FilePath content_test_data_path = GetTestDataFilePath(); |
| base::FilePath png_path = |
| content_test_data_path.AppendASCII("site_isolation/png-corp.png"); |
| ASSERT_TRUE(ReadTestFile(png_path, file_contents)); |
| |
| base::HistogramTester histograms; |
| { |
| base::RunLoop run_loop; |
| data_decoder::DecodeImageCallback callback = |
| base::BindLambdaForTesting([&](const SkBitmap& decoded_bitmap) { |
| EXPECT_EQ(100, decoded_bitmap.width()); |
| EXPECT_EQ(100, decoded_bitmap.height()); |
| run_loop.Quit(); |
| }); |
| |
| data_decoder::DataDecoder decoder; |
| data_decoder::DecodeImage( |
| &decoder, file_contents, data_decoder::mojom::ImageCodec::kDefault, |
| false, // shrink_to_fit |
| std::numeric_limits<uint32_t>::max(), // max_size_in_bytes |
| gfx::Size(), // desired_image_frame_size |
| std::move(callback)); |
| run_loop.Run(); |
| } |
| |
| FetchHistogramsFromChildProcesses(); |
| EXPECT_THAT( |
| histograms.GetTotalCountsForPrefix("Security.DataDecoder"), |
| UnorderedElementsAre( |
| Pair("Security.DataDecoder.Image.Reusable.EndToEndTime", 1), |
| Pair("Security.DataDecoder.Image.Reusable.ProcessOverhead", 1), |
| Pair("Security.DataDecoder.Image.DecodingTime", 1))); |
| |
| base::TimeDelta end_to_end_duration_estimate; |
| EXPECT_TRUE(GetSingleMeasurement( |
| histograms, "Security.DataDecoder.Image.Reusable.EndToEndTime", |
| end_to_end_duration_estimate)); |
| |
| base::TimeDelta overhead_estimate; |
| EXPECT_TRUE(GetSingleMeasurement( |
| histograms, "Security.DataDecoder.Image.Reusable.ProcessOverhead", |
| overhead_estimate)); |
| |
| base::TimeDelta decoding_duration_estimate; |
| EXPECT_TRUE(GetSingleMeasurement(histograms, |
| "Security.DataDecoder.Image.DecodingTime", |
| decoding_duration_estimate)); |
| |
| EXPECT_LE(decoding_duration_estimate, end_to_end_duration_estimate); |
| EXPECT_LE(overhead_estimate, end_to_end_duration_estimate); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, |
| NoCallbackAfterDestruction_Json) { |
| base::RunLoop run_loop; |
| |
| auto decoder = std::make_unique<data_decoder::DataDecoder>(); |
| auto* raw_decoder = decoder.get(); |
| |
| // Android's in-process parser can complete synchronously, so queue the |
| // delete task first unlike in the other tests. |
| base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon( |
| FROM_HERE, std::move(decoder)); |
| |
| bool got_callback = false; |
| raw_decoder->ParseJson( |
| "[1, 2, 3]", |
| base::BindOnce( |
| [](bool* got_callback, base::ScopedClosureRunner quit_closure_runner, |
| data_decoder::DataDecoder::ValueOrError result) { |
| *got_callback = true; |
| }, |
| // Pass the quit closure as a ScopedClosureRunner, so that the loop |
| // is quit if the callback is destroyed un-run or after it runs. |
| &got_callback, base::ScopedClosureRunner(run_loop.QuitClosure()))); |
| |
| run_loop.Run(); |
| |
| EXPECT_FALSE(got_callback); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, NoCallbackAfterDestruction_Xml) { |
| base::RunLoop run_loop; |
| |
| auto decoder = std::make_unique<data_decoder::DataDecoder>(); |
| bool got_callback = false; |
| decoder->ParseXml( |
| "<marquee>hello world</marquee>", |
| data_decoder::mojom::XmlParser::WhitespaceBehavior::kIgnore, |
| base::BindOnce( |
| [](bool* got_callback, base::ScopedClosureRunner quit_closure_runner, |
| data_decoder::DataDecoder::ValueOrError result) { |
| *got_callback = true; |
| }, |
| // Pass the quit closure as a ScopedClosureRunner, so that the loop |
| // is quit if the callback is destroyed un-run or after it runs. |
| &got_callback, base::ScopedClosureRunner(run_loop.QuitClosure()))); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon( |
| FROM_HERE, std::move(decoder)); |
| run_loop.Run(); |
| |
| EXPECT_FALSE(got_callback); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, |
| NoCallbackAfterDestruction_Gzip) { |
| base::RunLoop run_loop; |
| |
| auto decoder = std::make_unique<data_decoder::DataDecoder>(); |
| bool got_callback = false; |
| decoder->GzipCompress( |
| {{0x1, 0x1, 0x1, 0x1, 0x1, 0x1}}, |
| base::BindOnce( |
| [](bool* got_callback, base::ScopedClosureRunner quit_closure_runner, |
| base::expected<mojo_base::BigBuffer, std::string> result) { |
| *got_callback = true; |
| }, |
| // Pass the quit closure as a ScopedClosureRunner, so that the loop |
| // is quit if the callback is destroyed un-run or after it runs. |
| &got_callback, base::ScopedClosureRunner(run_loop.QuitClosure()))); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon( |
| FROM_HERE, std::move(decoder)); |
| run_loop.Run(); |
| |
| EXPECT_FALSE(got_callback); |
| } |
| |
| } // namespace content |