Refactor tests to use single source of truth ASCII diagram (#164)

- Less test bulk
 - Single source of truth: there is no mismatch between the comment and the expectations (we already caught a few mistakes in the conversion thanks to this benefit)
 - Easier to maintain and update
This commit is contained in:
Eric Eastwood 2023-04-07 12:52:41 -05:00 committed by GitHub
parent 954b22995a
commit 57d2cb3dd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 580 additions and 682 deletions

View File

@ -6,11 +6,9 @@ const escapeStringRegexp = require('escape-string-regexp');
const config = require('../lib/config');
const basePath = config.get('basePath');
assert(basePath);
const VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP = {
'#': 'r',
'!': 'roomid',
};
const {
VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP,
} = require('matrix-public-archive-shared/lib/reference-values');
// Create a regex string that will match a normal string or the URI encoded string or
// any combination of some characters being URI encoded. Only worries about characters

View File

@ -23,8 +23,14 @@ const DIRECTION = {
backward: 'b',
};
const VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP = {
'#': 'r',
'!': 'roomid',
};
module.exports = {
MS_LOOKUP,
TIME_PRECISION_VALUES,
DIRECTION,
VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP,
};

View File

@ -7,7 +7,9 @@ const { TIME_PRECISION_VALUES } = require('matrix-public-archive-shared/lib/refe
function qsToUrlPiece(qs) {
if (qs.toString()) {
return `?${qs.toString()}`;
// We allow `$` to be unencoded in the query string because it's a valid character
// in a Matrix event ID
return `?${qs.toString().replace(/%24/g, '$')}`;
} else {
return '';
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
'use strict';
const {
VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP,
} = require('matrix-public-archive-shared/lib/reference-values');
// http://archive.matrix.org/r/some-room:matrix.org/date/2022/11/16T23:59:59?at=$xxx
function parseArchiveUrlForRoom(archiveUrlForRoom) {
const urlObj = new URL(archiveUrlForRoom);
const urlPathname = urlObj.pathname;
// eslint-disable-next-line no-unused-vars -- It's more clear to leave `match` so we can see what we're destructuring from
const [match, roomIdOrAliasDescriptor, roomIdOrAliasUrlPart, urlDateTime] = urlPathname.match(
/\/(r|roomid)\/(.*?)\/date\/(.*)/
);
const [sigil] = Object.entries(VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP).find(
// eslint-disable-next-line no-unused-vars -- It's more clear to leave `sigil` so we can see what we're destructuring from
([sigil, entityDescriptor]) => roomIdOrAliasDescriptor === entityDescriptor
);
const roomIdOrAlias = `${sigil}${roomIdOrAliasUrlPart}`;
const continueAtEvent = urlObj.searchParams.get('at');
return {
roomIdOrAliasUrlPart,
roomIdOrAlias,
urlDateTime,
continueAtEvent,
};
}
module.exports = parseArchiveUrlForRoom;

View File

@ -0,0 +1,211 @@
'use strict';
const assert = require('assert');
function assertSequentialNumbersStartingFromOne(numbers) {
for (let i = 0; i < numbers.length - 1; i++) {
assert.equal(
numbers[i],
i,
`Expected numbers to be sequential starting from 1 but saw ${numbers[i]} at index ${i} from ${numbers}`
);
}
}
function findEventRangeFromBracketPairsInLine({ inputLine, regexPattern, eventLine }) {
assert(inputLine);
assert(regexPattern);
assert(eventLine);
const stringIndiciceToEventMap = new Map();
eventLine.replace(/\d+/g, (match, offset /*, string, groups*/) => {
const eventNumber = match;
stringIndiciceToEventMap.set(offset, parseInt(eventNumber, 10));
});
assert(stringIndiciceToEventMap.size > 0, `Expected to find at least one event in ${eventLine}`);
// Ensure the person defined the events in order
assertSequentialNumbersStartingFromOne(stringIndiciceToEventMap.values());
// Numbers can be multiple digits long. In order to lookup the eventNumber by the
// position of the closing bracket, we need to construct a map from the string index
// at the end of given eventNumber to the eventNumber.
//
// ex.
// ... <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
// [day1 ]
const stringIndiceNumberEndToEventMap = new Map();
Array.from(stringIndiciceToEventMap.entries()).forEach(([stringIndiceNumber, eventNumber]) => {
stringIndiceNumberEndToEventMap.set(
stringIndiceNumber + String(eventNumber).length,
eventNumber
);
});
const matchMap = new Map();
inputLine.replace(regexPattern, (match, numberLabel, offset /*, string, groups*/) => {
const startEventIndice = offset;
const endEventIndice = offset + match.length;
const startEventNumber = stringIndiciceToEventMap.get(startEventIndice);
const endEventNumber = stringIndiceNumberEndToEventMap.get(endEventIndice);
assert(
startEventNumber,
`For match ${numberLabel}, the opening bracket does not line up exactly with an event in the eventLine:\n` +
`${eventLine}\n` +
`${inputLine}\n` +
`Looking for event at startEventIndice=${startEventIndice} in ${JSON.stringify(
Object.fromEntries(stringIndiciceToEventMap.entries())
)}`
);
assert(
endEventNumber,
`For match ${numberLabel}, the closing bracket does not line up exactly with an event in the eventLine:\n` +
`${eventLine}\n` +
`${inputLine}\n` +
`Looking for event at endEventIndice=${endEventIndice} in ${JSON.stringify(
Object.fromEntries(stringIndiceNumberEndToEventMap.entries())
)}`
);
assert(
endEventNumber > startEventNumber,
`For match ${numberLabel}, expected endEventNumber=${endEventNumber} to be greater than startEventNumber=${startEventNumber}`
);
matchMap.set(numberLabel, {
startEventNumber,
endEventNumber,
});
});
return matchMap;
}
// Used in tests to parse a string that defines the structure of a room and the events
// in that room.
//
// ```
// const EXAMPLE_ROOM_DAY_MESSAGE_STRUCTURE_STRING = `
// [room1 ] [room2 ]
// 1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
// [day1 ] [day2 ]
// [page1 ]
// |--jump-fwd-4-messages-->|
// [page2 ]
// `;
// ```
function parseRoomDayMessageStructure(roomDayMessageStructureString) {
assert(roomDayMessageStructureString && roomDayMessageStructureString.length > 0);
// Strip the leading whitespace from each line
const rawLines = roomDayMessageStructureString.split(/\r?\n/);
// We choose the second line because the first line is likely to be empty
const numWhiteSpaceToStripFromEachLine = rawLines[1].match(/^\s*/)[0].length;
const lines = rawLines
.map((line) => line.slice(numWhiteSpaceToStripFromEachLine))
.filter((line) => {
return line.length > 0;
});
const roomLine = lines[0];
const eventLine = lines[1];
const dayLine = lines[2];
const pageLines = lines.filter((line) => line.match(/\[page\d+\s*\]/));
const dayToEventRangeMap = findEventRangeFromBracketPairsInLine({
inputLine: dayLine,
regexPattern: /\[day(\d+)\s*\]/g,
eventLine,
});
// Ensure the person defined the days in order
assertSequentialNumbersStartingFromOne(dayToEventRangeMap.keys());
// Make a map so it's easier to lookup which day an event is in
const eventToDayMap = new Map();
dayToEventRangeMap.forEach(({ startEventNumber, endEventNumber }, dayNumber) => {
for (let eventNumber = startEventNumber; eventNumber <= endEventNumber; eventNumber++) {
eventToDayMap.set(eventNumber, dayNumber);
}
});
const roomToEventRangeMap = findEventRangeFromBracketPairsInLine({
inputLine: roomLine,
regexPattern: /\[room(\d+)\s*\]/g,
eventLine,
});
// Ensure the person defined the rooms in order
assertSequentialNumbersStartingFromOne(roomToEventRangeMap.keys());
function getEventMetaFromEventNumber(eventNumber) {
const dayNumber = eventToDayMap.get(eventNumber);
assert(
dayNumber,
`Could not find event${eventNumber} associated with any day (check the brackets for "[dayX ]" to make sure it encompasses that event)\n` +
`${eventLine}\n` +
`${dayLine}\n` +
`eventToDayMap=${JSON.stringify(Object.fromEntries(eventToDayMap.entries()))}`
);
const eventRangeInDay = dayToEventRangeMap.get(dayNumber);
const event = {
eventNumber,
eventIndexInDay: eventNumber - eventRangeInDay.startEventNumber,
dayNumber,
};
return event;
}
// Get a list of events that should be in each room
const rooms = Array.from(roomToEventRangeMap.keys()).map((roomNumber) => {
const { startEventNumber, endEventNumber } = roomToEventRangeMap.get(roomNumber);
const events = [];
for (let eventNumber = startEventNumber; eventNumber <= endEventNumber; eventNumber++) {
events.push(getEventMetaFromEventNumber(eventNumber));
}
return {
events,
};
});
// Get a list of events that should be displayed on each page
const pages = pageLines.map((pageLine, index) => {
const pageToEventRangeMap = findEventRangeFromBracketPairsInLine({
inputLine: pageLine,
regexPattern: /\[page(\d+)\s*\]/g,
eventLine,
});
assert(
pageToEventRangeMap.size === 1,
`Expected to find exactly one page in line "${pageLine}" (found ${pageToEventRangeMap.size}). ` +
`Because pages can overlap on the events they display, they should be on their own lines`
);
// Ensure the person defined the pages in order
assert(Array.from(pageToEventRangeMap.keys())[0], index + 1);
const { startEventNumber, endEventNumber } = Array.from(pageToEventRangeMap.values())[0];
const events = [];
for (let eventNumber = startEventNumber; eventNumber <= endEventNumber; eventNumber++) {
events.push(getEventMetaFromEventNumber(eventNumber));
}
return {
events,
};
});
// Ensure that each page has the same number of events on it
const numEventsOnEachPage = pages.map((page) => page.events.length);
// The page limit is X but each page will display X + 1 messages because we fetch one
// extra to determine overflow.
const archiveMessageLimit = numEventsOnEachPage[0] - 1;
assert(
numEventsOnEachPage.every((numEvents) => numEvents === archiveMessageLimit + 1),
`Expected all pages to have the same number of events (archiveMessageLimit + 1) where archiveMessageLimit=${archiveMessageLimit} but found ${numEventsOnEachPage}`
);
return {
rooms,
archiveMessageLimit,
pages,
};
}
module.exports = parseRoomDayMessageStructure;