0

Add mojo conformance validation test to ts/js bindings

Design doc: go/conformancets

This change adds the mojo conformance validation test to the js/ts
bindings.

The test cases are read and parsed by the C++ side. A new mojo service
is introduced to plumb the test cases to the typescript.

On the typescript side, a new receiver/remote pair is created for each
test case, then the buffer is manually injected into the receiver's
message router. This allows us to bypass the underlying C-runtime and
directly pass the message to the js message handler. This does require
adding a new test method in mojojs' interface support library.

Unfortunately, error reporting/handling is very inconsistent in the
js bindings. A combination of console.error, console.assert, exception
throwing, and onError callbacks are used to report errors. Therefore
we need to override all those functions to determine whether or not
a mojo error has been reported for a given buffer.

Bug: 376760886
Change-Id: I5f21d933c0325cf937009df604ddf27d4cb89bdb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5817681
Commit-Queue: Fred Shih <ffred@chromium.org>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Rebekah Potter <rbpotter@chromium.org>
Reviewed-by: Yuzhu Shen <yzshen@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1379210}
This commit is contained in:
Fred Shih
2024-11-06 20:30:52 +00:00
committed by Chromium LUCI CQ
parent 77fffdfcf4
commit 040bcbd09c
11 changed files with 417 additions and 7 deletions

@ -2,11 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/342213636): Remove this and spanify to fix the errors.
#pragma allow_unsafe_buffers
#endif
#include <limits>
#include <utility>
@ -166,7 +161,7 @@ class TestWebUIController : public WebUIController {
{BindingsPolicyValue::kMojoWebUi}))
: WebUIController(web_ui) {
const base::span<const webui::ResourcePath> kMojoWebUiResources =
base::make_span(kWebUiMojoTestResources, kWebUiMojoTestResourcesSize);
base::make_span(kWebUiMojoTestResources);
web_ui->SetBindings(bindings);
#if BUILDFLAG(IS_CHROMEOS_ASH)

@ -0,0 +1,225 @@
// 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.
#include <array>
#include <fstream>
#include <memory>
#include "base/base_paths.h"
#include "base/containers/span.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/path_service.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "content/browser/webui/url_data_source_impl.h"
#include "content/browser/webui/web_ui_impl.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/browser/web_ui_controller_interface_binder.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/url_constants.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 "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/grit/web_ui_mojo_test_resources.h"
#include "content/test/grit/web_ui_mojo_test_resources_map.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/tests/validation_test_input_parser.h"
#include "mojo/public/interfaces/bindings/tests/validation_test_interfaces.mojom.h"
#include "ui/base/webui/resource_path.h"
namespace content {
namespace {
std::vector<base::FilePath> EnumerateFilesInDirectory(
const base::FilePath& dir,
const base::FilePath::StringType& pattern) {
std::vector<base::FilePath> test_files;
base::FileEnumerator e(dir, false, base::FileEnumerator::FILES, pattern);
e.ForEach([&test_files](const base::FilePath& file_path) -> void {
test_files.push_back(file_path);
});
return test_files;
}
// Reads the content of a file to a string. Crashes if reading failed.
std::string ReadFile(const base::FilePath& file_path) {
std::string content;
CHECK(base::ReadFileToString(file_path, &content));
return std::string(
base::TrimWhitespaceASCII(content, base::TrimPositions::TRIM_TRAILING));
}
std::vector<mojo::test::TestCasePtr> ParseTestCases() {
base::FilePath base_dir;
base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &base_dir);
const auto test_dir_relative_path = base::FilePath::FromASCII(
"mojo/public/interfaces/bindings/tests/data/"
"validation/");
base::FilePath test_cases_dir = base_dir.Append(test_dir_relative_path);
std::vector<base::FilePath> test_cases = EnumerateFilesInDirectory(
test_cases_dir, FILE_PATH_LITERAL("conformance_*.data"));
std::vector<mojo::test::TestCasePtr> parsed_test_cases;
for (const base::FilePath& test_file : test_cases) {
std::string test_data_raw = ReadFile(test_file);
std::vector<uint8_t> bytes;
size_t num_handles;
std::string error_msg;
CHECK(mojo::test::ParseValidationTestInput(test_data_raw, &bytes,
&num_handles, &error_msg));
std::string expectation = ReadFile(
test_file.RemoveFinalExtension().AddExtensionASCII("expected"));
parsed_test_cases.push_back(mojo::test::TestCase::New(
test_file.BaseName().RemoveFinalExtension().MaybeAsASCII(), bytes,
expectation));
}
CHECK(parsed_test_cases.size() > 0)
<< "No test cases were found, there might be an issue with pathing, "
"looked at: "
<< test_cases_dir;
return parsed_test_cases;
}
// WebUIController that sets up mojo bindings.
class ConformanceTestWebUIController : public WebUIController,
public mojo::test::PageHandlerFactory {
public:
explicit ConformanceTestWebUIController(WebUI* web_ui)
: WebUIController(web_ui) {
const base::span<const webui::ResourcePath> kMojoWebUiResources =
base::make_span(kWebUiMojoTestResources);
web_ui->SetBindings(
{BindingsPolicyValue::kMojoWebUi, BindingsPolicyValue::kWebUi});
WebUIDataSource* data_source = WebUIDataSource::CreateAndAdd(
web_ui->GetWebContents()->GetBrowserContext(),
"mojo-web-ui-conformance");
data_source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc,
"script-src chrome://resources 'self';");
data_source->AddResourcePaths(kMojoWebUiResources);
data_source->AddResourcePath("", IDR_WEB_UI_TS_TEST_CONFORMANCE_HTML);
}
ConformanceTestWebUIController(const ConformanceTestWebUIController&) =
delete;
ConformanceTestWebUIController& operator=(
const ConformanceTestWebUIController&) = delete;
void BindInterface(
mojo::PendingReceiver<mojo::test::PageHandlerFactory> receiver) {
page_factory_receiver_.reset();
page_factory_receiver_.Bind(std::move(receiver));
}
private:
void GetTestCases(GetTestCasesCallback cb) override {
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock()},
base::BindOnce(
[](GetTestCasesCallback cb,
const scoped_refptr<base::SequencedTaskRunner>& runner) {
auto test_cases = ParseTestCases();
runner->PostTask(
FROM_HERE,
base::BindOnce(std::move(cb), std::move(test_cases)));
},
std::move(cb), base::SequencedTaskRunner::GetCurrentDefault()));
}
mojo::Receiver<mojo::test::PageHandlerFactory> page_factory_receiver_{this};
WEB_UI_CONTROLLER_TYPE_DECL();
};
WEB_UI_CONTROLLER_TYPE_IMPL(ConformanceTestWebUIController)
class ConformanceTestWebUIControllerFactory : public WebUIControllerFactory {
public:
ConformanceTestWebUIControllerFactory() = default;
ConformanceTestWebUIControllerFactory(const TestWebUIControllerFactory&) =
delete;
ConformanceTestWebUIControllerFactory& operator=(
const TestWebUIControllerFactory&) = delete;
std::unique_ptr<WebUIController> CreateWebUIControllerForURL(
WebUI* web_ui,
const GURL& url) override {
return std::make_unique<ConformanceTestWebUIController>(web_ui);
}
WebUI::TypeID GetWebUIType(BrowserContext* browser_context,
const GURL& url) override {
if (!url.SchemeIs(kChromeUIScheme)) {
return WebUI::kNoWebUI;
}
return reinterpret_cast<WebUI::TypeID>(1);
}
bool UseWebUIForURL(BrowserContext* browser_context,
const GURL& url) override {
return true;
}
};
class ConformanceTestWebUIClient
: public ContentBrowserTestContentBrowserClient {
public:
ConformanceTestWebUIClient() = default;
ConformanceTestWebUIClient(const ConformanceTestWebUIClient&) = delete;
ConformanceTestWebUIClient& operator=(const ConformanceTestWebUIClient&) =
delete;
void RegisterBrowserInterfaceBindersForFrame(
RenderFrameHost* render_frame_host,
mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
RegisterWebUIControllerInterfaceBinder<mojo::test::PageHandlerFactory,
ConformanceTestWebUIController>(map);
}
};
class WebUIMojoConformanceTest : public ContentBrowserTest {
public:
WebUIMojoConformanceTest() = default;
WebUIMojoConformanceTest(const WebUIMojoConformanceTest&) = delete;
WebUIMojoConformanceTest& operator=(const WebUIMojoConformanceTest&) = delete;
protected:
void SetUpOnMainThread() override {
client_ = std::make_unique<ConformanceTestWebUIClient>();
}
void TearDownOnMainThread() override {}
ConformanceTestWebUIControllerFactory factory_;
ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_};
std::unique_ptr<ConformanceTestWebUIClient> client_;
};
IN_PROC_BROWSER_TEST_F(WebUIMojoConformanceTest, ConformanceTest) {
EXPECT_TRUE(NavigateToURL(shell(), GURL("data:,foo")));
GURL kTestUrl(GetWebUIURL("mojo-web-ui-conformance/"));
const std::string kTestScript = "runTest();";
EXPECT_TRUE(NavigateToURL(shell(), kTestUrl));
EXPECT_EQ(true, EvalJs(shell()->web_contents(), kTestScript));
}
} // namespace
} // namespace content

@ -1232,6 +1232,7 @@ preprocess_if_expr("preprocess_mojo_webui_test") {
in_folder = "./data"
out_folder = "$target_gen_dir/data"
in_files = [
"web_ui_mojo_conformance_test.ts",
"web_ui_mojo_ts_test.ts",
"web_ui_mojo_ts_test_converters.ts",
"web_ui_mojo_ts_test_mapped_types.ts",
@ -1240,10 +1241,21 @@ preprocess_if_expr("preprocess_mojo_webui_test") {
]
}
preprocess_if_expr("preprocess_mojo_webui_test_deps") {
in_folder = "$root_gen_dir/mojo/public/interfaces/bindings/tests"
out_folder = "$target_gen_dir/data"
in_files = [ "validation_test_interfaces.mojom-webui.ts" ]
deps = [
"//mojo/public/interfaces/bindings/tests:test_interfaces_ts__generator",
]
}
webui_ts_library("web_ui_mojo_test_build_ts") {
root_dir = "$target_gen_dir/data"
out_dir = "$target_gen_dir/data/tsc"
in_files = [
"validation_test_interfaces.mojom-webui.ts",
"web_ui_mojo_conformance_test.ts",
"web_ui_mojo_ts_test.ts",
"web_ui_mojo_ts_test_converters.ts",
"web_ui_mojo_ts_test_mapped_types.ts",
@ -1259,6 +1271,7 @@ webui_ts_library("web_ui_mojo_test_build_ts") {
deps = [ "//ui/webui/resources/mojo:build_ts" ]
extra_deps = [
":preprocess_mojo_webui_test",
":preprocess_mojo_webui_test_deps",
":web_ui_managed_interface_tests_bindings_ts__generator",
":web_ui_ts_test_mojo_bindings_ts__generator",
":web_ui_ts_test_other_mojo_bindings_ts__generator",
@ -1735,6 +1748,7 @@ test("content_browsertests") {
"../browser/webui/web_ui_browsertest.cc",
"../browser/webui/web_ui_managed_interface_browsertest.cc",
"../browser/webui/web_ui_mojo_browsertest.cc",
"../browser/webui/web_ui_mojo_conformance_browsertest.cc",
"../browser/webui/web_ui_navigation_browsertest.cc",
"../browser/webui/web_ui_security_browsertest.cc",
"../browser/worker_host/worker_browsertest.cc",
@ -1866,8 +1880,11 @@ test("content_browsertests") {
"//media:test_support",
"//media/webrtc",
"//mojo/core/embedder",
"//mojo/core/test:test_support",
"//mojo/public/cpp/bindings",
"//mojo/public/cpp/bindings/tests:mojo_public_bindings_test_utils",
"//mojo/public/cpp/test_support:test_utils",
"//mojo/public/interfaces/bindings/tests:test_interfaces",
"//net:quic_test_tools",
"//net:simple_quic_tools",
"//net:test_support",
@ -1950,6 +1967,7 @@ test("content_browsertests") {
data = [
"data/",
"//media/test/data/",
"//mojo/public/interfaces/bindings/tests/data/validation/",
"$root_gen_dir/third_party/perfetto/protos/perfetto/config/chrome/scenario_config.descriptor",
]
@ -1962,6 +1980,7 @@ test("content_browsertests") {
":gpu_process_bundle",
":network_process_bundle",
"//media/test:media_bundle_data",
"//mojo/public/interfaces/bindings/tests:validation_unittest_bundle_data",
"//testing/buildbot/filters:content_browsertests_filter_bundle_data",
]
} else {

@ -8112,6 +8112,8 @@ data/web_ui_dedicated_worker.js
data/web_ui_managed_interface_test.html
data/web_ui_managed_interface_test.test-mojom
data/web_ui_managed_interface_test.ts
data/web_ui_mojo_conformance_test.html
data/web_ui_mojo_conformance_test.ts
data/web_ui_mojo_native.html
data/web_ui_mojo_native.js
data/web_ui_mojo_test.html

@ -0,0 +1,5 @@
<html>
<body>
<script src="web_ui_mojo_conformance_test.js" type="module"></script>
</body>
</html>

@ -0,0 +1,126 @@
// 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 {mojo} from '//resources/mojo/mojo/public/js/bindings.js';
import {ConformanceTestInterfaceCallbackRouter, PageHandlerFactory} from './validation_test_interfaces.mojom-webui.js';
// TODO(ffred): These test cases do not match their associated expectation.
// Each case should be investigated and removed from this set.
const knownFailures = new Set([
'conformance_mthd0_incomplete_struct',
'conformance_mthd1_misaligned_struct',
'conformance_mthd13_good_2',
'conformance_mthd14_uknown_non_extensible_enum_value',
'conformance_mthd15_uknown_non_extensible_enum_array_value',
'conformance_mthd16_uknown_non_extensible_enum_map_key',
'conformance_mthd16_uknown_non_extensible_enum_map_value',
'conformance_mthd17_good',
'conformance_mthd19_exceed_recursion_limit',
'conformance_mthd2_multiple_pointers_to_same_struct',
'conformance_mthd2_overlapped_objects',
'conformance_mthd2_wrong_layout_order',
'conformance_mthd22_empty_nonextensible_enum_accepts_no_values',
'conformance_mthd23_array_of_optionals_less_than_necessary_bytes',
'conformance_mthd24_map_of_optionals_less_than_necessary_bytes',
'conformance_mthd3_array_num_bytes_huge',
'conformance_mthd3_array_num_bytes_less_than_array_header',
'conformance_mthd3_array_num_bytes_less_than_necessary_size',
'conformance_mthd3_misaligned_array',
'conformance_mthd4_multiple_pointers_to_same_array',
'conformance_mthd4_overlapped_objects',
'conformance_mthd4_wrong_layout_order',
'conformance_mthd5_good',
'conformance_mthd7_unmatched_array_elements',
'conformance_mthd7_unmatched_array_elements_nested',
'conformance_mthd9_good',
]);
class Fixture {
private endpoint: mojo.internal.interfaceSupport.Endpoint;
constructor() {
// We only need one end of the pipe
const pipes = Mojo.createMessagePipe();
const receiver = new ConformanceTestInterfaceCallbackRouter();
this.endpoint =
mojo.internal.interfaceSupport.getEndpointForReceiver(pipes.handle0);
// Binding is necessary to set up message routing.
receiver.$.bindHandle(this.endpoint);
}
// Return true if the test case passed. False otherwise.
runTestCase(buffer: ArrayBuffer): boolean {
const errors: Array<string> = [];
const oldConsoleError = console.error;
console.error = (errMsg: string) => errors.push(errMsg);
// Some mojo validations are done through console.assert and console.error.
// We need to capture errors on those channels to check whether or not
// incorrect buffers are correctly identified.
const oldConsoleAssert = console.assert;
console.assert = (condition: boolean, errMsg: string) => {
if (!condition) {
errors.push(errMsg);
}
};
// Other places use exception throwing to signal errors.
try {
mojo.internal.interfaceSupport.acceptBufferForTesting(
this.endpoint, buffer);
} catch (e) {
errors.push('' + e);
}
console.error = oldConsoleError;
console.assert = oldConsoleAssert;
return errors.length === 0;
}
}
async function runTest(): Promise<boolean> {
const remote = PageHandlerFactory.getRemote();
const testCases =
(await remote.getTestCases())
.testCases.sort((a, b) => a.testName.localeCompare(b.testName));
const failures = [];
const expectedFailures = [];
for (const testCase of testCases) {
const shouldSucceed = testCase.expectation === 'PASS';
const succeeded =
(new Fixture()).runTestCase(new Uint8Array(testCase.data).buffer);
if (succeeded !== shouldSucceed) {
if (knownFailures.has(testCase.testName)) {
expectedFailures.push(testCase.testName);
} else {
failures.push(testCase.testName);
}
} else {
if (knownFailures.has(testCase.testName)) {
throw new Error(
testCase.testName +
' is now passing, please remove from known failures');
}
}
}
if (expectedFailures.length > 0) {
console.log('tests continuing to fail: \n' + expectedFailures.join('\n'));
}
if (failures.length > 0) {
throw new Error('failed the following tests: \n' + failures.join('\n'));
}
return Promise.resolve(true);
}
// Exporting on |window| since this method is directly referenced by C++.
Object.assign(window, {runTest});

@ -37,6 +37,9 @@ This file specifies resources for content_browsertests.
<include name="IDR_WEB_UI_TS_TEST_OTHER_MAPPED_TYPES_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_ts_test_other_mapped_types.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_ts_test_other_mapped_types.js" />
<include name="IDR_WEB_UI_TS_TEST_CONVERTERS_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_ts_test_converters.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_ts_test_converters.js" />
<include name="IDR_WEB_UI_TS_TEST_TYPES_MOJOM_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_ts_test_types.test-mojom-webui.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_ts_test_types.test-mojom-webui.js" />
<include name="IDR_WEB_UI_TS_TEST_CONFORMANCE_HTML" file="data/web_ui_mojo_conformance_test.html" type="BINDATA" />
<include name="IDR_WEB_UI_TS_TEST_CONFORMANCE_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_conformance_test.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_conformance_test.js" />
<include name="IDR_WEB_UI_TS_TEST_VALIDATION_MOJO_JS" file="${root_gen_dir}/content/test/data/tsc/validation_test_interfaces.mojom-webui.js" use_base_dir="false" type="BINDATA" resource_path="validation_test_interfaces.mojom-webui.js" />
</includes>
</release>
</grit>

@ -60,6 +60,9 @@ mojom("test_interfaces") {
testonly = true
generate_java = true
generate_rust = true
webui_module_path = "/"
generate_legacy_js_bindings = true
sources = [
"math_calculator.mojom",
"no_module.mojom",
@ -283,6 +286,8 @@ mojom("test_mojom_import") {
testonly = true
generate_java = true
generate_rust = true
webui_module_path = "/"
generate_legacy_js_bindings = true
sources = [ "sample_import.mojom" ]
}
@ -304,6 +309,8 @@ mojom("test_mojom_import2") {
testonly = true
generate_java = true
generate_rust = true
webui_module_path = "/"
generate_legacy_js_bindings = true
sources = [ "sample_import2.mojom" ]
public_deps = [
":test_mojom_import",

@ -68,6 +68,21 @@ union UnionA {
bool b;
};
// Used by WebUI ts test.
struct TestCase {
string test_name;
// Bytes of a mojo message.
array<uint8> data;
// PASS for success. Every other string is the expected failure.
string? expectation;
};
// Used by WebUI ts test.
interface PageHandlerFactory {
// Gets the conformance test cases.
GetTestCases() => (array<TestCase> test_cases);
};
// This interface is used for testing bounds-checking in the mojom
// binding code. If you add a method please update the files
// ./data/validation/boundscheck_*. If you add a response please update

@ -336,10 +336,21 @@ mojo.internal.interfaceSupport.Endpoint = class {
}
};
/**
* @param {!mojo.internal.interfaceSupport.Endpoint} endpoint
* @param {!ArrayBuffer} buffer
* @export
*/
mojo.internal.interfaceSupport.acceptBufferForTesting = function(
endpoint, buffer) {
endpoint.router_.onMessageReceived_(buffer, []);
};
/**
* Creates a new Endpoint wrapping a given pipe handle.
*
* @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint} pipeOrEndpoint
* @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint}
* pipeOrEndpoint
* @param {boolean=} setNamespaceBit
* @return {!mojo.internal.interfaceSupport.Endpoint}
*/

@ -135,6 +135,8 @@ export namespace mojo {
interface Endpoint {}
function getEndpointForReceiver(handle: MojoHandle|Endpoint): Endpoint;
function acceptBufferForTesting(endpoint: Endpoint, buffer: ArrayBuffer):
void;
function bind(handle: Endpoint, name: string, scope: string): void;