chromium/third_party/blink/web_tests/http/tests/mojo/versioning.html

<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script type="module">
import {InterfaceVersionTest, InterfaceVersionTestReceiver, InterfaceVersionTestRemote, StructVersionTest_Deserialize} from '/gen/content/test/data/mojo_bindings_web_test.test-mojom.m.js';

async function waitForMessage(handle) {
  let watcher;
  await new Promise(r => {
    watcher = handle.watch({readable: true}, r);
  });
  watcher.cancel();
}

promise_test(async () => {
  // A manually serialized encoding of the StructVersionTest struct at version
  // 0, with just two string fields `a` and `b`. Assumes a little-endian host.
  const kTestStructV0 = new Uint32Array([
    24, 0,         // size and version
    16, 0, 24, 0,  // offset of string fields `a` and `b`
    9, 1, 120, 0,  // string `a` header and value 'x'
    9, 1, 121, 0,  // string `b` header and value 'y'
  ]);
  assert_object_equals(
    StructVersionTest_Deserialize(new DataView(kTestStructV0.buffer)),
    {a: 'x', b: 'y', c: null, d: null});

  // Adding field `c` as a version 1 struct
  const kTestStructV1 = new Uint32Array([
    32, 1,                 // size and version
    24, 0, 32,  0, 40, 0,  // offsets of string fields
    9,  1, 120, 0,         // string `a` header and value 'x'
    9,  1, 121, 0,         // string `b` header and value 'y'
    9,  1, 122, 0,         // string `c` header and value 'z'
  ]);
  assert_object_equals(
    StructVersionTest_Deserialize(new DataView(kTestStructV1.buffer)),
    {a: 'x', b: 'y', c: 'z', d: null});

  // Adding field `d` as a version 2 struct
  const kTestStructV2 = new Uint32Array([
    40, 2,                        // size and version
    32, 0, 40,  0, 48, 0, 56, 0,  // offsets of string fields
    9,  1, 120, 0,                // string `a` header and value 'x'
    9,  1, 121, 0,                // string `b` header and value 'y'
    9,  1, 122, 0,                // string `c` header and value 'z'
    9,  1, 119, 0,                // string `d` header and value 'w'
  ]);
  assert_object_equals(
    StructVersionTest_Deserialize(new DataView(kTestStructV2.buffer)),
    {a: 'x', b: 'y', c: 'z', d: 'w'});
}, 'current and older versions can be deserialized');

promise_test(async () => {
  // A manually serialized encoding of the StructVersionTest struct at an
  // imaginary version newer than the one defined in the committed mojom file we
  // have. This version has an extra string field unknown to our generated
  // bindings.
  const kTestStructV7 = new Uint32Array([
    48, 7,                                    // size and version
    40, 0, 48,       0, 56, 0, 64, 0, 72, 0,  // offset of string fields
    9,  1, 120,      0,                       // string `a` header and value 'x'
    9,  1, 121,      0,                       // string `b` header and value 'y'
    9,  1, 122,      0,                       // string `c` header and value 'z'
    9,  1, 119,      0,                       // string `d` header and value 'w'
    11, 3, 0x6c6f6c, 0,  // unknown new string field header and contents
  ]);

  // The `e` field is not present because it is not part of our local mojom
  // definition and is therefore not known to our generated deserializer. We can
  // still deserialize what we know about though.
  assert_object_equals(
    StructVersionTest_Deserialize(new DataView(kTestStructV7.buffer)),
    {a: 'x', b: 'y', c: 'z', d: 'w'});
}, 'newer versions can be partially deserialized');

promise_test(async () => {
  const kTestTooSmall = new Uint32Array([8, 0]);
  assert_throws_js(
    Error,
    () => StructVersionTest_Deserialize(new DataView(kTestTooSmall.buffer)));
}, 'reject struct encoding that is too small even for version 0');

promise_test(async () => {
  const kTestWrongSize = new Uint32Array([
    32, 2,         // size and version. size should be 40 but isn't.
    16, 0, 24, 0,  // offsets of string fields
    9, 1, 120, 0,  // string `a` header and value 'x'
    9, 1, 121, 0,  // string `b` header and value 'y'
  ]);
  assert_throws_js(
    Error,
    () => StructVersionTest_Deserialize(new DataView(kTestWrongSize.buffer)));
}, 'reject struct encoding whose size does not match the version');

class InterfaceVersionTestImpl {
  constructor() {
    const {handle0, handle1} = Mojo.createMessagePipe();
    this.remoteHandle_ = handle0;
    this.receiver_ = new InterfaceVersionTestReceiver(this);
    this.receiver_.$.bindHandle(handle1);
    this.nextFooResolvers_ = [];
  }

  sendRawMessage(message) {
    this.remoteHandle_.writeMessage(message, []);
  }

  receiveNextFoo() {
    return new Promise(resolve => {
      this.nextFooResolvers_.push(resolve);
    });
  }

  foo(x, y) {
    const resolvers = this.nextFooResolvers_;
    this.nextFooResolvers = [];
    for (const r of resolvers) {
      r({z: y, w: x});
    }
    return {z: y, w: x};
  }
}

promise_test(async () => {
  const impl = new InterfaceVersionTestImpl;

  const kTestMessageV0 = new Uint32Array([
    // Mojo message header. This is request 0 of message ID 0 on interface 0,
    // expecting a reply. See the definition of MessageHeaderV1 within
    // mojo/public/cpp/bindings/lib/message_internal.h.
    32, 1, 0, 0, 1, 0, 0, 0,

    16, 0,  // Foo params struct size and version
    42, 0,  // value of `x` and padding
  ]);

  // The implementation returns the inputs x,y swapped, so z=y and w=x. The
  // message was v0 and does not include y, so we should have z=0 and w=42.
  impl.sendRawMessage(kTestMessageV0);
  const {z: z0, w: w0} = await impl.receiveNextFoo();
  assert_equals(z0, 0);
  assert_equals(w0, 42);

  // Current version (1)
  const kTestMessageV1 = new Uint32Array([
    32, 1, 0, 0, 1, 0, 1, 0,  // Mojo message header. See above.
    16, 1,                    // Foo params struct size and version
    42, 37,                   // `x` and `y` values
  ]);

  impl.sendRawMessage(kTestMessageV1);
  const {z: z1, w: w1} = await impl.receiveNextFoo();
  assert_equals(z1, 37);
  assert_equals(w1, 42);

  // Imaginary future version 2.
  const kTestMessageV2 = new Uint32Array([
    32, 1, 0, 0, 1, 0, 2, 0,  // Mojo message header
    24, 2,                    // Foo params struct size and version
    42, 37,                   // `x` and `y` values
    19, 0,                    // new unknown field value
  ]);

  impl.sendRawMessage(kTestMessageV2);
  const {z: z2, w: w2} = await impl.receiveNextFoo();
  assert_equals(z2, 37);
  assert_equals(w2, 42);
}, 'interface receivers can handle requests from all versions old and new');

promise_test(async () => {
  const {handle0, handle1} = Mojo.createMessagePipe();
  const remote = new InterfaceVersionTestRemote(handle1);

  const kTestReplyV0 = new Uint32Array([
    // Mojo message header. This is reply 0 for message ID 0 on interface 0.
    // See the definition of MessageHeaderV1 within
    // mojo/public/cpp/bindings/lib/message_internal.h.
    32, 1, 0, 0, 2, 0, 0, 0,

    16, 0,  // Foo reply struct size and version
    19, 0,  // `z` value and padding
  ]);

  // Send the expected reply before we actually send the corresponding request.
  // The receiver won't see it until after the request happens below.
  handle0.writeMessage(kTestReplyV0, []);
  const {z: z0, w: w0} = await remote.foo(1, 2);
  assert_equals(z0, 19);
  assert_equals(w0, 0);

  const kTestReplyV1 = new Uint32Array([
    32, 1, 0, 0, 2, 0, 1, 0,  // Mojo message header
    16, 1,                    // Foo reply struct size and version
    19, 7,                    // `z` and `w` values
  ]);

  // Send the expected reply before we actually send the corresponding request.
  // The receiver won't see it until after the request happens below.
  handle0.writeMessage(kTestReplyV1, []);
  const {z: z1, w: w1} = await remote.foo(1, 2);
  assert_equals(z1, 19);
  assert_equals(w1, 7);

  const kTestReplyV2 = new Uint32Array([
    32, 1, 0, 0, 2, 0, 2, 0,  // Mojo message header
    24, 2,                    // Foo reply struct size and version
    19, 7,                    // `z` and `w` values
    0x12345678,               // new unknown field value
  ]);

  // Send the expected reply before we actually send the corresponding request.
  // The receiver won't see it until after the request happens below.
  handle0.writeMessage(kTestReplyV2, []);
  const {z: z2, w: w2} = await remote.foo(1, 2);
  assert_equals(z2, 19);
  assert_equals(w2, 7);
}, 'interface remotes can handle responses from all versions old and new');

promise_test(async () => {
  const {handle0, handle1} = Mojo.createMessagePipe();
  const remote = new InterfaceVersionTestRemote(handle0);
  remote.foo(1, 2);
  await waitForMessage(handle1);

  const {result, buffer} = handle1.readMessage();
  assert_equals(result, Mojo.RESULT_OK);

  // First some quick general validity checks.
  const data = new DataView(buffer);
  assert_equals(data.getUint32(0, true), 32);  // message header size
  assert_equals(data.getUint32(4, true), 1);   // message header version

  // Ensure the parameter structure reflects version 1 of the Foo message, since
  // that's the current version in the mojom we're using.
  assert_equals(data.getUint32(32, true), 16);  // Foo request size
  assert_equals(data.getUint32(36, true), 1);   // Foo request version
}, 'structures properly encode their current version in outgoing messages');
</script>