Skip to content

Commit

Permalink
[beacon-api] Unify python helper set_beacon*.py get_beacon*.py
Browse files Browse the repository at this point in the history
as suggested in [3]

[3]: https://crrev.com/c/3789544/10/third_party/blink/web_tests/wpt_internal/pending_beacon/resources/pending_beacon-helper.js#68

Bug: 1293679
Change-Id: I5285c1665ae8d00cf96a40ebf0f37db799764c94
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3820181
Reviewed-by: Fergal Daly <fergal@chromium.org>
Commit-Queue: Ming-Ying Chung <mych@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1034329}
  • Loading branch information
mingyc authored and Chromium LUCI CQ committed Aug 12, 2022
1 parent ff4716e commit 2b36b82
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 138 deletions.
Expand Up @@ -7,32 +7,32 @@

promise_test(async t => {
const uuid = token();
const url = generateSetBeaconCountURL(uuid);
const url = generateSetBeaconURL(uuid);

// Create and send a beacon.
const beacon = new PendingGetBeacon(url);
beacon.sendNow();

await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});
}, 'sendNow() sends a beacon immediately.');

promise_test(async t => {
const uuid = token();
const url = generateSetBeaconCountURL(uuid);
const url = generateSetBeaconURL(uuid);

// Create and send a beacon.
const beacon = new PendingGetBeacon(url);
beacon.sendNow();
await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});

// Try to send the beacon again, and verify no beacon arrives.
beacon.sendNow();
await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});
}, 'sendNow() doesn\'t send the same beacon twice.');

promise_test(async t => {
const uuid = token();
const url = generateSetBeaconCountURL(uuid);
const url = generateSetBeaconURL(uuid);

// Create and send 1st beacon.
const beacon1 = new PendingGetBeacon(url);
Expand All @@ -42,5 +42,5 @@ promise_test(async t => {
const beacon2 = new PendingGetBeacon(url);
beacon2.sendNow();

await expectBeaconCount(uuid, 2);
await expectBeacon(uuid, {count: 2});
}, 'sendNow() sends multiple beacons.');
Expand Up @@ -23,7 +23,7 @@ promise_test(async t => {
document.body.removeChild(iframe);

// The iframe should have sent two beacons.
await expectBeaconCount(uuid, 2);
await expectBeacon(uuid, {count: 2});
}, 'Verify that a discarded document sends its beacons.');

promise_test(async t => {
Expand All @@ -42,9 +42,9 @@ promise_test(async t => {
await iframe_load_promise;

// The iframe should have sent one beacon.
await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});

// Delete the document and verify no more beacons are sent.
document.body.removeChild(iframe);
await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});
}, 'Verify that a discarded document does not send an already sent beacon.');
Expand Up @@ -9,20 +9,20 @@ const baseUrl = `${location.protocol}//${location.host}`;

parallelPromiseTest(async t => {
const uuid = token();
const url = generateSetBeaconCountURL(uuid);
const url = generateSetBeaconURL(uuid);

let beacon = new PendingGetBeacon('/');

beacon.setURL(url);
assert_equals(beacon.url, url);
beacon.sendNow();

await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});
}, 'PendingGetBeacon is sent to the updated URL');

parallelPromiseTest(async t => {
const uuid = token();
const url = generateSetBeaconCountURL(uuid);
const url = generateSetBeaconURL(uuid);

let beacon = new PendingGetBeacon('/0');

Expand All @@ -36,5 +36,5 @@ parallelPromiseTest(async t => {

beacon.sendNow();

await expectBeaconCount(uuid, 1);
await expectBeacon(uuid, {count: 1});
}, 'PendingGetBeacon is sent to the last updated URL');
Expand Up @@ -6,34 +6,19 @@
'use strict';

// Test empty data.
postBeaconSendDataTest(
BeaconDataType.String, '',
/*expectNoData=*/ true, 'Sent empty String, and server got no data.');
postBeaconSendDataTest(
BeaconDataType.ArrayBuffer, '',
/*expectNoData=*/ true, 'Sent empty ArrayBuffer, and server got no data.');
postBeaconSendDataTest(
BeaconDataType.FormData, '',
/*expectNoData=*/ false, 'Sent empty form payload, and server got "".');
postBeaconSendDataTest(
BeaconDataType.URLSearchParams, 'testkey=',
/*expectNoData=*/ false, 'Sent empty URLparams, and server got "".');
for (const dataType in BeaconDataType) {
postBeaconSendDataTest(
dataType, '', `Sent empty ${dataType}, and server got no data.`, {
expectNoData: true,
});
}

// Test small payload.
postBeaconSendDataTest(
BeaconDataType.String, generateSequentialData(0, 1024),
/*expectNoData=*/ false, 'Encoded and sent in POST request.');
postBeaconSendDataTest(
BeaconDataType.ArrayBuffer, generateSequentialData(0, 1024),
/*expectNoData=*/ false, 'Encoded and sent in POST request.');
// Skip CRLF characters which will be normalized by FormData.
postBeaconSendDataTest(
BeaconDataType.FormData, generateSequentialData(0, 1024, '\n\r'),
/*expectNoData=*/ false, 'Encoded and sent in POST request.');
// Skip reserved URI characters.
postBeaconSendDataTest(
BeaconDataType.URLSearchParams,
'testkey=' + generateSequentialData(0, 1024, ';,/?:@&=+$'),
/*expectNoData=*/ false, 'Encoded and sent in POST request.');
for (const [dataType, skipCharset] of Object.entries(
BeaconDataTypeToSkipCharset)) {
postBeaconSendDataTest(
dataType, generateSequentialData(0, 1024, skipCharset),
'Encoded and sent in POST request.');
}

// TODO(crbug.com/1293679): Test large payload.
@@ -0,0 +1,30 @@
"""An HTTP request handler for WPT that handles /get_beacon.py requests."""

import json

_BEACON_ID_KEY = b"uuid"
_BEACON_DATA_PATH = "beacon_data"


def main(request, response):
"""Retrieves the beacon data keyed by the given uuid from server storage.
The response content is a JSON string in one of the following formats:
- "{'data': ['abc', null, '123',...]}"
- "{'data': []}" indicates that no data has been set for this uuid.
"""
if _BEACON_ID_KEY not in request.GET:
response.status = 400
return "Must provide a UUID to store beacon data"
uuid = request.GET.first(_BEACON_ID_KEY)

with request.server.stash.lock:
body = {'data': []}
data = request.server.stash.take(key=uuid, path=_BEACON_DATA_PATH)
if data:
body['data'] = data
# The stash is read-once/write-once, so it has to be put back after
# reading if `data` is not None.
request.server.stash.put(
key=uuid, value=data, path=_BEACON_DATA_PATH)
return [(b'Content-Type', b'text/plain')], json.dumps(body)

This file was deleted.

This file was deleted.

Expand Up @@ -16,6 +16,16 @@ const BeaconDataType = {
URLSearchParams: 'URLSearchParams',
};

/** @enum {string} */
const BeaconDataTypeToSkipCharset = {
String: '',
ArrayBuffer: '',
FormData: '\n\r', // CRLF characters will be normalized by FormData
URLSearchParams: ';,/?:@&=+$', // reserved URI characters
};

const BEACON_PAYLOAD_KEY = 'payload';

// Creates beacon data of the given `dataType` from `data`.
// @param {string} data - A string representation of the beacon data. Note that
// it cannot contain UTF-16 surrogates for all `BeaconDataType` except BLOB.
Expand All @@ -28,10 +38,15 @@ function makeBeaconData(data, dataType) {
return new TextEncoder().encode(data).buffer;
case BeaconDataType.FormData:
const formData = new FormData();
formData.append('payload', data);
if (data.length > 0) {
formData.append(BEACON_PAYLOAD_KEY, data);
}
return formData;
case BeaconDataType.URLSearchParams:
return new URLSearchParams(data);
if (data.length > 0) {
return new URLSearchParams(`${BEACON_PAYLOAD_KEY}=${data}`);
}
return new URLSearchParams();
default:
throw Error(`Unsupported beacon dataType: ${dataType}`);
}
Expand All @@ -48,58 +63,76 @@ function generateSequentialData(begin, end, skip) {
return String.fromCharCode(...codeUnits);
}

function generateSetBeaconCountURL(uuid) {
return `/pending_beacon/resources/set_beacon_count.py?uuid=${uuid}`;
function generateSetBeaconURL(uuid) {
return `/pending_beacon/resources/set_beacon.py?uuid=${uuid}`;
}

async function poll(f, expected) {
const interval = 400; // milliseconds.
while (true) {
const result = await f();
if (result === expected) {
if (expected(result)) {
return result;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
}

async function expectBeaconCount(uuid, expected) {
poll(async () => {
const res = await fetch(
`/pending_beacon/resources/get_beacon_count.py?uuid=${uuid}`,
{cache: 'no-store'});
return await res.json();
}, expected);
}
// Waits until the `options.count` number of beacon data available from the
// server. Defaults to 1.
// If `options.data` is set, it will be used to compare with the data from the
// response.
async function expectBeacon(uuid, options) {
const expectedCount = options && options.count ? options.count : 1;

async function expectBeaconData(uuid, expected, options) {
poll(async () => {
const res = await fetch(
`/pending_beacon/resources/get_beacon_count.py?uuid=${uuid}`,
{cache: 'no-store'});
let data = await res.text();
if (options && options.percentDecoded) {
// application/x-www-form-urlencoded serializer encodes space as '+'
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
data = data.replace(/\+/g, '%20');
return decodeURIComponent(data);
}
return data;
}, expected);
const res = await poll(
async () => {
const res = await fetch(
`/pending_beacon/resources/get_beacon.py?uuid=${uuid}`,
{cache: 'no-store'});
return await res.json();
},
(res) => {
return res.data.length == expectedCount;
});
if (!options || !options.data) {
return;
}

const decoder = options && options.percentDecoded ? (s) => {
// application/x-www-form-urlencoded serializer encodes space as '+'
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
s = s.replace(/\+/g, '%20');
return decodeURIComponent(s);
} : (s) => s;

assert_equals(
res.data.length, options.data.length,
`The size of beacon data ${
res.data.length} from server does not match expected value ${
options.data.length}.`);
for (let i = 0; i < options.data.length; i++) {
assert_equals(
decoder(res.data[i]), options.data[i],
'The beacon data does not match expected value.');
}
}

function postBeaconSendDataTest(dataType, testData, expectEmpty, description) {
function postBeaconSendDataTest(dataType, testData, description, options) {
parallelPromiseTest(async t => {
const expectNoData = options && options.expectNoData;
const uuid = token();
const url = `/pending_beacon/resources/set_beacon_data.py?uuid=${uuid}`;
let beacon = new PendingPostBeacon(url);
assert_equals(beacon.method, 'POST');
const url = generateSetBeaconURL(uuid);
const beacon = new PendingPostBeacon(url);
assert_equals(beacon.method, 'POST', 'must be POST to call setData().');

beacon.setData(makeBeaconData(testData, dataType));
beacon.sendNow();

const expected = expectEmpty ? '<NO-DATA>' : testData;
const percentDecoded = dataType === BeaconDataType.URLSearchParams;
await expectBeaconData(uuid, expected, {percentDecoded: percentDecoded});
}, `Beacon data of type "${dataType}": ${description}`);
const expectedData = expectNoData ? null : testData;
const percentDecoded =
!expectNoData && dataType === BeaconDataType.URLSearchParams;
await expectBeacon(
uuid, {count: 1, data: [expectedData], percentDecoded: percentDecoded});
}, `PendingPostBeacon(${dataType}): ${description}`);
}
@@ -0,0 +1,45 @@
"""An HTTP request handler for WPT that handles /set_beacon.py requests."""

_BEACON_ID_KEY = b"uuid"
_BEACON_DATA_PATH = "beacon_data"
_BEACON_FORM_PAYLOAD_KEY = b"payload"
_BEACON_BODY_PAYLOAD_KEY = "payload="


def main(request, response):
"""Stores the given beacon's data keyed by uuid in the server.
For GET request, this handler assumes no data.
For POST request, this handler extracts data from request body:
- Content-Type=multipart/form-data: data keyed by 'payload'.
- the entire request body.
Multiple data can be added for the same uuid.
The data is stored as UTF-8 format.
"""
if _BEACON_ID_KEY not in request.GET:
response.status = 400
return "Must provide a UUID to store beacon data"
uuid = request.GET.first(_BEACON_ID_KEY)

data = None
if request.method == u"POST":
if b"multipart/form-data" in request.headers.get(b"Content-Type", b""):
if _BEACON_FORM_PAYLOAD_KEY in request.POST:
data = request.POST.first(_BEACON_FORM_PAYLOAD_KEY).decode(
'utf-8')
elif request.body:
data = request.body.decode('utf-8')
if data.startswith(_BEACON_BODY_PAYLOAD_KEY):
data = data.split(_BEACON_BODY_PAYLOAD_KEY)[1]

with request.server.stash.lock:
saved_data = request.server.stash.take(key=uuid, path=_BEACON_DATA_PATH)
if not saved_data:
saved_data = [data]
else:
saved_data.append(data)
request.server.stash.put(
key=uuid, value=saved_data, path=_BEACON_DATA_PATH)
return ((200, "OK"), [], "")

0 comments on commit 2b36b82

Please sign in to comment.