diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7714339 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Cyrano is a Node.js service implementing the **EFP2 (Ethernet Fencing Protocol 2)** — a UDP-based protocol for networked fencing scoring machines. It bridges scoring machines (Masters) with competition management software (Managers), results servers, and display clients. + +## Commands + +```bash +npm test # Run all tests (requires --experimental-vm-modules) +npm test -- tests/path/to/file.test.js # Run a single test file +npm test -- --testNamePattern="pattern" # Run tests matching a name +npm run coverage # Run tests with coverage report +npm run build # Bundle with esbuild → build/ +npm start # Run built app +npm run clean # Remove node_modules and reinstall +``` + +## Architecture + +**Entry point:** `index.js` — UDP server on port 50100 + +**Protocol pipeline:** `cylex.js` (tokenizer) → `cyrano.js` (processor) → `commands/` (handlers) + +- `src/protocol/cyranoTokens.js` — Protocol constants (`EFP2`, `|` separator) +- `src/protocol/cylex.js` — Splits raw messages by `|` into token arrays +- `src/protocol/cyrano.js` — Validates EFP2 header, dispatches to command handlers +- `src/commands/index.js` — Auto-loads all command handlers from the `commands/` directory +- `src/commands/*.js` — Individual command handlers (HELLO, PING, STOP, MSG, DISP) + +## EFP2 Protocol Reference + +**Message format:** `|EFP2|COMMAND|param1|param2|...|` (ASCII, fields delimited by `|`) + +### Network Topology + +| Node | IP | Ports | +|------|----|-------| +| Scoring machines Server | 172.20.0.1 | 50100 (from Masters), 50103 (from Managers) | +| Backup Scoring machines Server | 172.20.0.2 | same as primary | +| Master (piste x) | 172.20.x.1 (x=1..60) | 50100 (from Servers), 50101 (from Additional devices) | +| Additional devices (piste x) | 172.20.x.y (y=1..255) | any | +| Manager | 172.20.0.y (y=9..32) | any | +| Server of the Results | 172.20.0.8 | 50100 (from Servers), 50103 (from Managers), 50104 (from Clients) | +| Clients | 172.20.129–254.x | any | + +### All Commands + +| Command | Description | Params | +|---------|-------------|--------| +| `HELLO` | Node is online | 0–1 (optional piste code) | +| `PING` | Check node presence; recipient must reply `\|EFP2\|HELLO\|\|` | 0 | +| `STOP` | Disconnect / stop receiving | 0 | +| `DISP` | Set new bout on piste | 18 (see below) | +| `INFO` | Piste state (score, time, lights, cards, etc.) | ~40 fields | +| `ACK` | Bout result accepted | 0 | +| `NAK` | Bout result rejected | 0 | +| `NEXT` | Referee requests next match | 1 (piste code) | +| `PREV` | Referee requests previous match | 1 (piste code) | +| `TEAM` | Team member list for one side | 19 (piste, side, 3 members + reserve, 9 round assignments, unique ID) | +| `GETTEAM` | Request team list from Server of Results | 2 (piste code, side) | +| `REPLACE` | Team member substitution | 3 (piste code, side, fencer number 1–3) | +| `BOUTSTOP` | Cancel current DISP / clear piste | 1 (piste code) | +| `MSG` | Text message to display | 2 (piste code or `ALL`, message text ≤128 chars) | +| `STANDBY` | Switch apparatus to sleep mode | 1 (piste code) | +| `BROKEN` | Lost contact with piste | 1 (piste code) | +| `DENY` | Deny a request | reason string | +| `UPDATED` | XML competition data updated | 2 (event ID, competition code) | + +### TEAM structure (20 tokens) +`members` is an array of 4: indices 0–2 are the active fencers, index 3 is the reserve. +`rounds` is a 9-element array of fencer numbers (1, 2, or 3) indicating who fences each round. + +### DISP fields (fields 3–20) +PisteCode, EventID, CompetitionCode, Phase, Order, BoutID, TimeBegin, Stopwatch, +Right ID, Right Name, Right Nation, Right MemberID, Right MemberName, +Left ID, Left Name, Left Nation, Left MemberID, Left MemberName + +### Piste codes +Numeric `1`–`59` for standard pistes; named `BLUE`, `YELLOW`, `GREEN`, `RED`, `FINAL` for final area pistes. + +### Competition codes +`EIM`/`EIW`/`ETM`/`ETW` (épée), `FIM`/`FIW`/`FTM`/`FTW` (foil), `SIM`/`SIW`/`STM`/`STW` (sabre), `MIX` + +### Master state machine +States: `Standalone` → `Indefinite` → `Not active` → `Halt`/`Fencing`/`Pause`/`Waiting`/`Ending` +- Enters `Not active` only on receiving `DISP` +- Enters `Ending` on manual bout deactivation; sends `INFO` every second for up to 4s awaiting `ACK`/`NAK` +- `ACK` → `Not active`; `NAK` or timeout → `Waiting` + +## Testing + +Tests mirror the `src/` structure under `tests/`. Jest runs with ES module support (`--experimental-vm-modules`). No transpilation is needed for tests — the project uses native ES modules. diff --git a/demo/run.js b/demo/run.js new file mode 100644 index 0000000..1654b48 --- /dev/null +++ b/demo/run.js @@ -0,0 +1,332 @@ +/** + * EFP2 Demo — Bout Lifecycle + * + * Simulates a Server (competition software) and a Master (scoring machine) + * communicating over UDP on loopback, following the EFP2 protocol spec. + * + * Network topology (loopback): + * Server 127.0.0.1:50100 — listens for Masters, manages bouts + * Master 127.0.0.1:50101 — piste RED scoring machine + * + * Lifecycle demonstrated: + * 1. Master → HELLO RED (comes online) + * 2. Server → DISP RED … (assigns a bout) + * 3. Master → INFO … state=A (Not active, waiting for referee) + * 4. Master → INFO … state=F (Fencing, score advances each tick) + * 5. Master → INFO … state=E ×n (Ending, sends every 1 s for up to 4 s) + * 6. Server → ACK (accepts the result) + * 7. Master → INFO … state=A (returns to Not active) + * + * State codes used in the INFO 'state' field: + * 'A' — Not active (DISP received, bout not yet started) + * 'F' — Fencing (bout in progress) + * 'E' — Ending (referee deactivated, awaiting ACK/NAK) + */ + +import dgram from 'dgram'; +import { compose, process } from '../src/protocol/cyrano.js'; + +const SERVER_PORT = 50100; +const MASTER_PORT = 50101; +const HOST = '127.0.0.1'; +const PISTE = 'RED'; + +// ── Colour helpers ──────────────────────────────────────────────────────────── +const reset = s => `\x1b[0m${s}\x1b[0m`; +const dim = s => `\x1b[2m${s}\x1b[0m`; +const bold = s => `\x1b[1m${s}\x1b[0m`; +const cyan = s => `\x1b[36m${s}\x1b[0m`; +const yellow = s => `\x1b[33m${s}\x1b[0m`; +const green = s => `\x1b[32m${s}\x1b[0m`; +const red = s => `\x1b[31m${s}\x1b[0m`; + +function log(actor, dir, summary, wire = null) { + const label = actor === 'SERVER' ? cyan(bold('SERVER')) : yellow(bold('MASTER')); + const arrow = dir === 'tx' ? '→' : '←'; + const detail = wire ? ` ${dim(wire.trim())}` : ''; + console.log(` ${label} ${arrow} ${summary}${detail}`); +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// ── Send a command object as a UDP datagram ─────────────────────────────────── +function send(socket, commandObj, toPort, toHost = HOST) { + const wire = compose(commandObj); + const buf = Buffer.from(wire, 'ascii'); + return new Promise((resolve, reject) => + socket.send(buf, toPort, toHost, err => err ? reject(err) : resolve(wire)) + ); +} + +// ── Fixture data (matches the cyrano.test.js examples) ─────────────────────── +const RIGHT_FENCER = { + id: '33', + name: 'IVANOV Sidor', + teamInfo: 'RUS', + teamMemberId: '', + teamMemberName: '', +}; +const LEFT_FENCER = { + id: '531', + name: 'LIMON Jua', + teamInfo: 'FRA', + teamMemberId: '', + teamMemberName: '', +}; + +const BOUT_HEADER = { + piste: PISTE, + eventId: '24', + competitionCode: 'EIM', + competitionPhase: 'T32', + boutOrderInPhase: '1', + roundNumber: '1', + boutId: '32', + beginTime: '14:45', + stopwatch: '3:00', + refereeRemoteControl: '0', + priority: 'N', + callTechnician: '0', + callVideoEngineer:'0', + callDoctor: '0', + callDT: '0', + reverse: '0', + standby: '0', +}; + +/** Build an INFO command object for the given state and scores. */ +function makeInfo(state, rightScore, leftScore, rightLamp = '0', leftLamp = '0') { + return { + command: 'INFO', + ...BOUT_HEADER, + state, + rightFencer: { + ...RIGHT_FENCER, + score: String(rightScore), + yellowCard: '0', + redCards: '0', + blackCard: '0', + usedVideo: '0', + lamp: rightLamp, + whiteLamp: '0', + }, + leftFencer: { + ...LEFT_FENCER, + score: String(leftScore), + yellowCard: '0', + redCards: '0', + blackCard: '0', + usedVideo: '0', + lamp: leftLamp, + whiteLamp: '0', + }, + }; +} + +/** Build a DISP command object for the bout assignment. */ +function makeDisp() { + return { + command: 'DISP', + piste: PISTE, + eventId: '24', + competitionCode: 'EIM', + competitionPhase: 'T32', + boutOrderInPhase: '1', + boutId: '32', + beginTime: '14:45', + stopwatch: '3:00', + rightFencer: { + id: RIGHT_FENCER.id, + name: RIGHT_FENCER.name, + teamInfo: RIGHT_FENCER.teamInfo, + teamId: '', + teamMemberName: '', + }, + leftFencer: { + id: LEFT_FENCER.id, + name: LEFT_FENCER.name, + teamInfo: LEFT_FENCER.teamInfo, + teamId: '', + teamMemberName: '', + }, + }; +} + +// ── SERVER ──────────────────────────────────────────────────────────────────── +async function startServer() { + const socket = dgram.createSocket('udp4'); + + socket.on('message', async (raw) => { + const wire = raw.toString('ascii'); + const obj = process(wire); + + switch (obj.command) { + + case 'HELLO': { + log('SERVER', 'rx', `${bold('HELLO')} piste=${obj.pisteCode}`, wire); + // Assign a bout to this piste + await delay(400); + const disp = makeDisp(); + const sent = await send(socket, disp, MASTER_PORT); + log('SERVER', 'tx', + `${bold('DISP')} ${obj.pisteCode} — ` + + `${RIGHT_FENCER.name} (${RIGHT_FENCER.teamInfo}) ` + + `vs ${LEFT_FENCER.name} (${LEFT_FENCER.teamInfo})`, sent); + break; + } + + case 'INFO': { + const rs = obj.rightFencer.score; + const ls = obj.leftFencer.score; + const st = obj.state; + + const stateLabel = { A: 'Not active', F: 'Fencing', E: 'Ending' }[st] ?? st; + log('SERVER', 'rx', + `${bold('INFO')} state=${bold(stateLabel)} ` + + `${cyan(RIGHT_FENCER.name + ' ' + rs)} ${dim('–')} ${yellow(LEFT_FENCER.name + ' ' + ls)}`, + wire); + + if (st === 'E') { + // Accept the bout result + await delay(700); + const sent = await send(socket, { command: 'ACK' }, MASTER_PORT); + log('SERVER', 'tx', `${bold('ACK')} ${green('result accepted')}`, sent); + } + break; + } + } + }); + + await new Promise(resolve => socket.bind(SERVER_PORT, HOST, resolve)); + console.log(cyan(` Server bound ${HOST}:${SERVER_PORT}`)); + return socket; +} + +// ── MASTER ──────────────────────────────────────────────────────────────────── +async function startMaster(serverSocket) { + const socket = dgram.createSocket('udp4'); + let endingTimer = null; + + socket.on('message', async (raw) => { + const wire = raw.toString('ascii'); + const obj = process(wire); + + switch (obj.command) { + + case 'DISP': { + log('MASTER', 'rx', `${bold('DISP')} bout assigned`, wire); + + // Transition to Not active — send initial INFO + const infoA = makeInfo('A', 0, 0); + const sentA = await send(socket, infoA, SERVER_PORT); + log('MASTER', 'tx', `${bold('INFO')} state=Not active score=0–0`, sentA); + + // Short pause then referee starts the bout + await delay(500); + simulateFencing(socket); + break; + } + + case 'ACK': { + log('MASTER', 'rx', `${bold('ACK')}`, wire); + clearInterval(endingTimer); + endingTimer = null; + + // Return to Not active with final score + const infoFinal = makeInfo('A', 5, 3); + const sentFinal = await send(socket, infoFinal, SERVER_PORT); + log('MASTER', 'tx', + `${bold('INFO')} state=Not active score=5–3 ${green('(bout complete)')}`, + sentFinal); + + await delay(300); + console.log('\n' + green(bold(' ✓ Bout lifecycle complete')) + '\n'); + socket.close(); + serverSocket.close(); + break; + } + + case 'NAK': { + log('MASTER', 'rx', `${bold('NAK')} ${red('result rejected — entering Waiting')}`, wire); + clearInterval(endingTimer); + socket.close(); + serverSocket.close(); + break; + } + } + }); + + /** Score progression for a 5–3 bout (right wins). + * Each step: [rightScore, leftScore, rightLamp, leftLamp] */ + const SCORE_STEPS = [ + [1, 0, '1', '0'], + [1, 1, '0', '1'], + [2, 1, '1', '0'], + [3, 1, '1', '0'], + [3, 2, '0', '1'], + [4, 2, '1', '0'], + [4, 3, '0', '1'], + [5, 3, '1', '0'], // final touch — right wins + ]; + + async function simulateFencing(socket) { + console.log(dim('\n ── Fencing ──────────────────────────────')); + for (const [rs, ls, rl, ll] of SCORE_STEPS) { + await delay(600); + const info = makeInfo('F', rs, ls, rl, ll); + const sent = await send(socket, info, SERVER_PORT); + log('MASTER', 'tx', + `${bold('INFO')} state=Fencing score=${cyan(rs)}–${yellow(ls)}`, + sent); + } + + // Referee deactivates bout — enter Ending state + console.log(dim('\n ── Ending (awaiting ACK/NAK) ────────────')); + let endingCount = 0; + endingTimer = setInterval(async () => { + endingCount++; + const info = makeInfo('E', 5, 3); + const sent = await send(socket, info, SERVER_PORT); + log('MASTER', 'tx', + `${bold('INFO')} state=Ending score=5–3 ${dim(`(${endingCount}/4)`)}`, + sent); + + if (endingCount >= 4) { + // Timeout — no ACK received, transition to Waiting + clearInterval(endingTimer); + log('MASTER', 'tx', + `${red(bold('timeout'))} no ACK after 4 s — would enter Waiting state`); + socket.close(); + serverSocket.close(); + } + }, 1000); + } + + await new Promise(resolve => socket.bind(MASTER_PORT, HOST, resolve)); + console.log(yellow(` Master bound ${HOST}:${MASTER_PORT}`) + + dim(` (piste ${PISTE})`)); + return socket; +} + +// ── Entry point ─────────────────────────────────────────────────────────────── +async function main() { + console.log(bold('\n ══════════════════════════════════════')); + console.log(bold(' EFP2 Demo — Bout Lifecycle ')); + console.log(bold(' ══════════════════════════════════════\n')); + + const serverSocket = await startServer(); + const masterSocket = await startMaster(serverSocket); + + // Master initiates contact + console.log(dim('\n ── Handshake ─────────────────────────')); + await delay(100); + const helloWire = await send(masterSocket, { command: 'HELLO', pisteCode: PISTE }, SERVER_PORT); + log('MASTER', 'tx', `${bold('HELLO')} pisteCode=${PISTE}`, helloWire); +} + +main().catch(err => { + console.error(red(bold('Fatal: ')) + err.message); + process.exit(1); +}); diff --git a/package.json b/package.json index 389399d..812d245 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "clean": "rm -rf ./node_modules && npm i", "build": "./node_modules/.bin/esbuild --platform=node --minify --bundle index.js --outdir=build", "start": "node ./build/index.js", + "demo": "./node_modules/.bin/esbuild --platform=node --bundle demo/run.js --outfile=build/demo.js && node build/demo.js", "test": "node --experimental-vm-modules --input-type=module node_modules/jest/bin/jest.js", "coverage": "jest --collect-coverage" }, diff --git a/src/commands/ack.js b/src/commands/ack.js new file mode 100644 index 0000000..ee12cc3 --- /dev/null +++ b/src/commands/ack.js @@ -0,0 +1,30 @@ +// noinspection SpellCheckingInspection +export const ACK_COMMAND = "ACK"; +const LENGTH = 0; + +export const register = (commandDictionary) => { + commandDictionary[ACK_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[ACK_COMMAND] = build; + return builderDictionary; +} + +export const build = ({}) => { + return [ACK_COMMAND]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${ACK_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": ACK_COMMAND, + }; +} diff --git a/src/commands/boutstop.js b/src/commands/boutstop.js new file mode 100644 index 0000000..7cf60bf --- /dev/null +++ b/src/commands/boutstop.js @@ -0,0 +1,31 @@ +// noinspection SpellCheckingInspection +export const BOUTSTOP_COMMAND = "BOUTSTOP"; +const LENGTH = 1; + +export const register = (commandDictionary) => { + commandDictionary[BOUTSTOP_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[BOUTSTOP_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste }) => { + return [BOUTSTOP_COMMAND, piste]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${BOUTSTOP_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": BOUTSTOP_COMMAND, + "piste": localTokens.shift(), + }; +} diff --git a/src/commands/broken.js b/src/commands/broken.js new file mode 100644 index 0000000..2b3f037 --- /dev/null +++ b/src/commands/broken.js @@ -0,0 +1,31 @@ +// noinspection SpellCheckingInspection +export const BROKEN_COMMAND = "BROKEN"; +const LENGTH = 1; + +export const register = (commandDictionary) => { + commandDictionary[BROKEN_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[BROKEN_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste }) => { + return [BROKEN_COMMAND, piste]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${BROKEN_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": BROKEN_COMMAND, + "piste": localTokens.shift(), + }; +} diff --git a/src/commands/deny.js b/src/commands/deny.js new file mode 100644 index 0000000..2e3a63b --- /dev/null +++ b/src/commands/deny.js @@ -0,0 +1,31 @@ +// noinspection SpellCheckingInspection +export const DENY_COMMAND = "DENY"; +const LENGTH = 1; + +export const register = (commandDictionary) => { + commandDictionary[DENY_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[DENY_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ reason }) => { + return [DENY_COMMAND, reason]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${DENY_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": DENY_COMMAND, + "reason": localTokens.shift(), + }; +} diff --git a/src/commands/disp.js b/src/commands/disp.js index 63b1df8..9bbaf0c 100644 --- a/src/commands/disp.js +++ b/src/commands/disp.js @@ -7,6 +7,20 @@ export const register = (commandDictionary) => { return commandDictionary; } +export const registerBuilder = (builderDictionary) => { + builderDictionary[DISP_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste, eventId, competitionCode, competitionPhase, boutOrderInPhase, boutId, beginTime, stopwatch, rightFencer, leftFencer }) => { + return [ + DISP_COMMAND, + piste, eventId, competitionCode, competitionPhase, boutOrderInPhase, boutId, beginTime, stopwatch, + rightFencer.id, rightFencer.name, rightFencer.teamInfo, rightFencer.teamId, rightFencer.teamMemberName, + leftFencer.id, leftFencer.name, leftFencer.teamInfo, leftFencer.teamId, leftFencer.teamMemberName, + ]; +} + const parse = (tokens) => { const length = (tokens || []).length; if (length !== LENGTH) { diff --git a/src/commands/getteam.js b/src/commands/getteam.js new file mode 100644 index 0000000..5759aa9 --- /dev/null +++ b/src/commands/getteam.js @@ -0,0 +1,32 @@ +// noinspection SpellCheckingInspection +export const GETTEAM_COMMAND = "GETTEAM"; +const LENGTH = 2; + +export const register = (commandDictionary) => { + commandDictionary[GETTEAM_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[GETTEAM_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste, side }) => { + return [GETTEAM_COMMAND, piste, side]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${GETTEAM_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": GETTEAM_COMMAND, + "piste": localTokens.shift(), + "side": localTokens.shift(), + }; +} diff --git a/src/commands/hello.js b/src/commands/hello.js index e1ea8d1..8fd2682 100644 --- a/src/commands/hello.js +++ b/src/commands/hello.js @@ -8,6 +8,22 @@ export const register = (commandDictionary) => { return commandDictionary; } +export const registerBuilder = (builderDictionary) => { + builderDictionary[HELLO_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ pisteCode }) => { + const params = pisteCode ? [pisteCode] : []; + const length = params.length; + + if (length < MIN_LENGTH || length > MAX_LENGTH) { + throw new Error(`Incompatible build params for >${HELLO_COMMAND}<. Expected ${MIN_LENGTH} or ${MAX_LENGTH}, Got: ${length}`); + } + + return [HELLO_COMMAND, ...params]; +} + const parse = (tokens) => { const localTokens = tokens || []; const length = localTokens.length; diff --git a/src/commands/index.js b/src/commands/index.js index 385450e..1c345c4 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -1,7 +1,40 @@ const dictionary = {} +const builders = {} +require("./ack").register(dictionary); +require("./ack").registerBuilder(builders); +require("./boutstop").register(dictionary); +require("./boutstop").registerBuilder(builders); +require("./broken").register(dictionary); +require("./broken").registerBuilder(builders); +require("./deny").register(dictionary); +require("./deny").registerBuilder(builders); require("./disp").register(dictionary); +require("./disp").registerBuilder(builders); +require("./getteam").register(dictionary); +require("./getteam").registerBuilder(builders); require("./hello").register(dictionary); +require("./hello").registerBuilder(builders); +require("./info").register(dictionary); +require("./info").registerBuilder(builders); +require("./msg").register(dictionary); +require("./msg").registerBuilder(builders); +require("./nak").register(dictionary); +require("./nak").registerBuilder(builders); +require("./next").register(dictionary); +require("./next").registerBuilder(builders); require("./ping").register(dictionary); +require("./ping").registerBuilder(builders); +require("./prev").register(dictionary); +require("./prev").registerBuilder(builders); +require("./replace").register(dictionary); +require("./replace").registerBuilder(builders); +require("./standby").register(dictionary); +require("./standby").registerBuilder(builders); require("./stop").register(dictionary); -require("./msg").register(dictionary); +require("./stop").registerBuilder(builders); +require("./team").register(dictionary); +require("./team").registerBuilder(builders); +require("./updated").register(dictionary); +require("./updated").registerBuilder(builders); export default dictionary; +export { builders }; diff --git a/src/commands/info.js b/src/commands/info.js new file mode 100644 index 0000000..b40bd35 --- /dev/null +++ b/src/commands/info.js @@ -0,0 +1,86 @@ +// noinspection SpellCheckingInspection +export const INFO_COMMAND = "INFO"; +const LENGTH = 42; + +export const register = (commandDictionary) => { + commandDictionary[INFO_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[INFO_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste, eventId, competitionCode, competitionPhase, boutOrderInPhase, roundNumber, boutId, beginTime, stopwatch, state, refereeRemoteControl, priority, callTechnician, callVideoEngineer, callDoctor, callDT, reverse, standby, rightFencer, leftFencer }) => { + return [ + INFO_COMMAND, + piste, eventId, competitionCode, competitionPhase, boutOrderInPhase, roundNumber, boutId, beginTime, stopwatch, + state, refereeRemoteControl, priority, callTechnician, callVideoEngineer, callDoctor, callDT, reverse, standby, + rightFencer.id, rightFencer.name, rightFencer.teamInfo, rightFencer.teamMemberId, rightFencer.teamMemberName, + rightFencer.score, rightFencer.yellowCard, rightFencer.redCards, rightFencer.blackCard, rightFencer.usedVideo, + rightFencer.lamp, rightFencer.whiteLamp, + leftFencer.id, leftFencer.name, leftFencer.teamInfo, leftFencer.teamMemberId, leftFencer.teamMemberName, + leftFencer.score, leftFencer.yellowCard, leftFencer.redCards, leftFencer.blackCard, leftFencer.usedVideo, + leftFencer.lamp, leftFencer.whiteLamp, + ]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${INFO_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": INFO_COMMAND, + "piste": localTokens.shift(), + "eventId": localTokens.shift(), + "competitionCode": localTokens.shift(), + "competitionPhase": localTokens.shift(), + "boutOrderInPhase": localTokens.shift(), + "roundNumber": localTokens.shift(), + "boutId": localTokens.shift(), + "beginTime": localTokens.shift(), + "stopwatch": localTokens.shift(), + "state": localTokens.shift(), + "refereeRemoteControl": localTokens.shift(), + "priority": localTokens.shift(), + "callTechnician": localTokens.shift(), + "callVideoEngineer": localTokens.shift(), + "callDoctor": localTokens.shift(), + "callDT": localTokens.shift(), + "reverse": localTokens.shift(), + "standby": localTokens.shift(), + "rightFencer": { + "id": localTokens.shift(), + "name": localTokens.shift(), + "teamInfo": localTokens.shift(), + "teamMemberId": localTokens.shift(), + "teamMemberName": localTokens.shift(), + "score": localTokens.shift(), + "yellowCard": localTokens.shift(), + "redCards": localTokens.shift(), + "blackCard": localTokens.shift(), + "usedVideo": localTokens.shift(), + "lamp": localTokens.shift(), + "whiteLamp": localTokens.shift(), + }, + "leftFencer": { + "id": localTokens.shift(), + "name": localTokens.shift(), + "teamInfo": localTokens.shift(), + "teamMemberId": localTokens.shift(), + "teamMemberName": localTokens.shift(), + "score": localTokens.shift(), + "yellowCard": localTokens.shift(), + "redCards": localTokens.shift(), + "blackCard": localTokens.shift(), + "usedVideo": localTokens.shift(), + "lamp": localTokens.shift(), + "whiteLamp": localTokens.shift(), + }, + }; +} diff --git a/src/commands/msg.js b/src/commands/msg.js index 7362bcb..9418ba3 100644 --- a/src/commands/msg.js +++ b/src/commands/msg.js @@ -7,6 +7,15 @@ export const register = (commandDictionary) => { return commandDictionary; } +export const registerBuilder = (builderDictionary) => { + builderDictionary[MSG_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste, message }) => { + return [MSG_COMMAND, piste, message]; +} + const parse = (tokens) => { const localTokens = tokens || []; const length = localTokens.length; diff --git a/src/commands/nak.js b/src/commands/nak.js new file mode 100644 index 0000000..fe47e97 --- /dev/null +++ b/src/commands/nak.js @@ -0,0 +1,30 @@ +// noinspection SpellCheckingInspection +export const NAK_COMMAND = "NAK"; +const LENGTH = 0; + +export const register = (commandDictionary) => { + commandDictionary[NAK_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[NAK_COMMAND] = build; + return builderDictionary; +} + +export const build = ({}) => { + return [NAK_COMMAND]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${NAK_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": NAK_COMMAND, + }; +} diff --git a/src/commands/next.js b/src/commands/next.js new file mode 100644 index 0000000..b592b46 --- /dev/null +++ b/src/commands/next.js @@ -0,0 +1,31 @@ +// noinspection SpellCheckingInspection +export const NEXT_COMMAND = "NEXT"; +const LENGTH = 1; + +export const register = (commandDictionary) => { + commandDictionary[NEXT_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[NEXT_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste }) => { + return [NEXT_COMMAND, piste]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${NEXT_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": NEXT_COMMAND, + "piste": localTokens.shift(), + }; +} diff --git a/src/commands/ping.js b/src/commands/ping.js index 3fed469..480b71a 100644 --- a/src/commands/ping.js +++ b/src/commands/ping.js @@ -7,6 +7,15 @@ export const register = (commandDictionary) => { return commandDictionary; } +export const registerBuilder = (builderDictionary) => { + builderDictionary[PING_COMMAND] = build; + return builderDictionary; +} + +export const build = ({}) => { + return [PING_COMMAND]; +} + const parse = (tokens) => { const localTokens = tokens || []; const length = localTokens.length; diff --git a/src/commands/prev.js b/src/commands/prev.js new file mode 100644 index 0000000..09ef041 --- /dev/null +++ b/src/commands/prev.js @@ -0,0 +1,31 @@ +// noinspection SpellCheckingInspection +export const PREV_COMMAND = "PREV"; +const LENGTH = 1; + +export const register = (commandDictionary) => { + commandDictionary[PREV_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[PREV_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste }) => { + return [PREV_COMMAND, piste]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${PREV_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": PREV_COMMAND, + "piste": localTokens.shift(), + }; +} diff --git a/src/commands/replace.js b/src/commands/replace.js new file mode 100644 index 0000000..ce70c9c --- /dev/null +++ b/src/commands/replace.js @@ -0,0 +1,33 @@ +// noinspection SpellCheckingInspection +export const REPLACE_COMMAND = "REPLACE"; +const LENGTH = 3; + +export const register = (commandDictionary) => { + commandDictionary[REPLACE_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[REPLACE_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste, side, fencerNumber }) => { + return [REPLACE_COMMAND, piste, side, fencerNumber]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${REPLACE_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": REPLACE_COMMAND, + "piste": localTokens.shift(), + "side": localTokens.shift(), + "fencerNumber": localTokens.shift(), + }; +} diff --git a/src/commands/standby.js b/src/commands/standby.js new file mode 100644 index 0000000..f0fe70c --- /dev/null +++ b/src/commands/standby.js @@ -0,0 +1,31 @@ +// noinspection SpellCheckingInspection +export const STANDBY_COMMAND = "STANDBY"; +const LENGTH = 1; + +export const register = (commandDictionary) => { + commandDictionary[STANDBY_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[STANDBY_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste }) => { + return [STANDBY_COMMAND, piste]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${STANDBY_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": STANDBY_COMMAND, + "piste": localTokens.shift(), + }; +} diff --git a/src/commands/stop.js b/src/commands/stop.js index 1e62ab3..741c26f 100644 --- a/src/commands/stop.js +++ b/src/commands/stop.js @@ -7,6 +7,15 @@ export const register = (commandDictionary) => { return commandDictionary; } +export const registerBuilder = (builderDictionary) => { + builderDictionary[STOP_COMMAND] = build; + return builderDictionary; +} + +export const build = ({}) => { + return [STOP_COMMAND]; +} + const parse = (tokens) => { const localTokens = tokens || []; const length = localTokens.length; diff --git a/src/commands/team.js b/src/commands/team.js new file mode 100644 index 0000000..74ade66 --- /dev/null +++ b/src/commands/team.js @@ -0,0 +1,59 @@ +// noinspection SpellCheckingInspection +export const TEAM_COMMAND = "TEAM"; +const LENGTH = 20; + +export const register = (commandDictionary) => { + commandDictionary[TEAM_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[TEAM_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ piste, side, members, rounds, uniqueId }) => { + return [ + TEAM_COMMAND, + piste, side, + members[0].id, members[0].name, + members[1].id, members[1].name, + members[2].id, members[2].name, + members[3].id, members[3].name, + ...rounds, + uniqueId, + ]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${TEAM_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": TEAM_COMMAND, + "piste": localTokens.shift(), + "side": localTokens.shift(), + "members": [ + { "id": localTokens.shift(), "name": localTokens.shift() }, + { "id": localTokens.shift(), "name": localTokens.shift() }, + { "id": localTokens.shift(), "name": localTokens.shift() }, + { "id": localTokens.shift(), "name": localTokens.shift() }, + ], + "rounds": [ + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + localTokens.shift(), + ], + "uniqueId": localTokens.shift(), + }; +} diff --git a/src/commands/updated.js b/src/commands/updated.js new file mode 100644 index 0000000..8da8b13 --- /dev/null +++ b/src/commands/updated.js @@ -0,0 +1,32 @@ +// noinspection SpellCheckingInspection +export const UPDATED_COMMAND = "UPDATED"; +const LENGTH = 2; + +export const register = (commandDictionary) => { + commandDictionary[UPDATED_COMMAND] = parse; + return commandDictionary; +} + +export const registerBuilder = (builderDictionary) => { + builderDictionary[UPDATED_COMMAND] = build; + return builderDictionary; +} + +export const build = ({ eventId, competitionCode }) => { + return [UPDATED_COMMAND, eventId, competitionCode]; +} + +const parse = (tokens) => { + const localTokens = tokens || []; + const length = localTokens.length; + + if (length !== LENGTH) { + throw new Error(`Incompatible command tokens for >${UPDATED_COMMAND}<. Expected ${LENGTH}, Got: ${length}`); + } + + return { + "command": UPDATED_COMMAND, + "eventId": localTokens.shift(), + "competitionCode": localTokens.shift(), + }; +} diff --git a/src/protocol/cyrano.js b/src/protocol/cyrano.js index 3940bce..bcede34 100644 --- a/src/protocol/cyrano.js +++ b/src/protocol/cyrano.js @@ -1,7 +1,7 @@ // main class to take a packet of information and break it into import {tokenize} from "./cylex"; -import {PROTOCOL} from "./cyranoTokens"; -import commandDictionary from "../commands"; +import {PROTOCOL, PROTOCOL_SEPERATOR} from "./cyranoTokens"; +import commandDictionary, {builders as builderDictionary} from "../commands"; export const process = (rawMessage) => { const tokens = tokenize(rawMessage); @@ -16,3 +16,12 @@ export const process = (rawMessage) => { throw new Error(`Unsupported protocol >${protocolToken}<`); } } + +export const compose = (commandObject) => { + const build = builderDictionary[commandObject.command]; + if (!build) { + throw new Error(`No builder registered for command >${commandObject.command}<`); + } + const tokens = build(commandObject); + return PROTOCOL_SEPERATOR + [PROTOCOL, ...tokens].join(PROTOCOL_SEPERATOR) + PROTOCOL_SEPERATOR; +} diff --git a/tests/commands/ack.test.js b/tests/commands/ack.test.js new file mode 100644 index 0000000..9c28f7a --- /dev/null +++ b/tests/commands/ack.test.js @@ -0,0 +1,47 @@ +import {ACK_COMMAND, register, registerBuilder, build} from "../../src/commands/ack"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[ACK_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[ACK_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI"]); }) + .toThrowError(`Incompatible command tokens for >${ACK_COMMAND}<. Expected 0, Got: 1`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }).not.toThrow(); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse([]); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(ACK_COMMAND); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[ACK_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command only', () => { + expect(build({})).toEqual(['ACK']); + }); +}); diff --git a/tests/commands/boutstop.test.js b/tests/commands/boutstop.test.js new file mode 100644 index 0000000..60cf428 --- /dev/null +++ b/tests/commands/boutstop.test.js @@ -0,0 +1,81 @@ +import {BOUTSTOP_COMMAND, register, registerBuilder, build} from "../../src/commands/boutstop"; + +const EXAMPLE_BOUTSTOP_TOKENS = [ + 'RED' +]; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + const result = commandDictionary[BOUTSTOP_COMMAND]; + expect( + typeof result + ).toEqual( + 'function' + ); + }); +}); + +describe('#parse', () => { + const parse = register({})[BOUTSTOP_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + + test('problem with token length for this parser', () => { + expect( + () => { + parse(["HI", "THERE"]); + } + ).toThrowError(`Incompatible command tokens for >${BOUTSTOP_COMMAND}<. Expected 1, Got: 2`); + }); + + test('no issue with null tokens', () => { + expect( + () => { + parse(null); + } + ).toThrowError(`Incompatible command tokens for >${BOUTSTOP_COMMAND}<. Expected 1, Got: 0`); + }); + }); + + describe("valid parameters", () => { + describe("boutstop command with a piste code", () => { + beforeEach(() => { + const tokens = [...EXAMPLE_BOUTSTOP_TOKENS]; + parsedResult = parse(tokens); + }); + + test('has the command in the result', () => { + expect( + parsedResult['command'] + ).toEqual( + BOUTSTOP_COMMAND + ) + }); + + test('reads the piste', () => { + expect( + parsedResult['piste'] + ).toEqual( + 'RED' + ) + }); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[BOUTSTOP_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command and piste', () => { + expect(build({ piste: 'RED' })).toEqual(['BOUTSTOP', 'RED']); + }); +}); diff --git a/tests/commands/broken.test.js b/tests/commands/broken.test.js new file mode 100644 index 0000000..04ad052 --- /dev/null +++ b/tests/commands/broken.test.js @@ -0,0 +1,51 @@ +import {BROKEN_COMMAND, register, registerBuilder, build} from "../../src/commands/broken"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[BROKEN_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[BROKEN_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI", "THERE"]); }) + .toThrowError(`Incompatible command tokens for >${BROKEN_COMMAND}<. Expected 1, Got: 2`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${BROKEN_COMMAND}<. Expected 1, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['3']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(BROKEN_COMMAND); + }); + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('3'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[BROKEN_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command and piste', () => { + expect(build({ piste: '3' })).toEqual(['BROKEN', '3']); + }); +}); diff --git a/tests/commands/deny.test.js b/tests/commands/deny.test.js new file mode 100644 index 0000000..aec9a09 --- /dev/null +++ b/tests/commands/deny.test.js @@ -0,0 +1,51 @@ +import {DENY_COMMAND, register, registerBuilder, build} from "../../src/commands/deny"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[DENY_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[DENY_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI", "THERE"]); }) + .toThrowError(`Incompatible command tokens for >${DENY_COMMAND}<. Expected 1, Got: 2`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${DENY_COMMAND}<. Expected 1, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['Piste does not exists']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(DENY_COMMAND); + }); + test('reads the reason', () => { + expect(parsedResult['reason']).toEqual('Piste does not exists'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[DENY_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command and reason', () => { + expect(build({ reason: 'Piste does not exists' })).toEqual(['DENY', 'Piste does not exists']); + }); +}); diff --git a/tests/commands/disp.test.js b/tests/commands/disp.test.js index 8d08952..1d1321d 100644 --- a/tests/commands/disp.test.js +++ b/tests/commands/disp.test.js @@ -1,4 +1,4 @@ -import {DISP_COMMAND, register} from "../../src/commands/disp"; +import {DISP_COMMAND, register, registerBuilder, build} from "../../src/commands/disp"; const EXAMPLE_DISP_TOKENS = [ 'RED', '24', 'EIM', 'T32', '1', '32', "14:45", "3:00", @@ -121,47 +121,24 @@ describe('#parse', () => { describe('right fencer', () => { let rightFencer = null; - // "33", " IVANOV Sidor", 'CAN', '', '', beforeEach(() => { rightFencer = parsedResult['rightFencer']; }); test('right fencer id', () => { - expect( - rightFencer["id"] - ).toEqual( - "33" - ) + expect(rightFencer["id"]).toEqual("33") }); test('name', () => { - expect( - rightFencer["name"] - ).toEqual( - " IVANOV Sidor" - ) + expect(rightFencer["name"]).toEqual(" IVANOV Sidor") }); test('team info', () => { - expect( - rightFencer["teamInfo"] - ).toEqual( - "CAN" - ) + expect(rightFencer["teamInfo"]).toEqual("CAN") }); - test('team id', () => { - expect( - rightFencer["teamId"] - ).toEqual( - "rightTeamId" - ) + expect(rightFencer["teamId"]).toEqual("rightTeamId") }); - test('team Member Name', () => { - expect( - rightFencer["teamMemberName"] - ).toEqual( - "rightTeamMemberName" - ) + expect(rightFencer["teamMemberName"]).toEqual("rightTeamMemberName") }); }); describe('left fencer', () => { @@ -171,42 +148,40 @@ describe('#parse', () => { }); test('fencer id', () => { - expect( - leftFencer["id"] - ).toEqual( - "531" - ) + expect(leftFencer["id"]).toEqual("531") }); test('name', () => { - expect( - leftFencer["name"] - ).toEqual( - "LIMON Jua" - ) + expect(leftFencer["name"]).toEqual("LIMON Jua") }); test('team info', () => { - expect( - leftFencer["teamInfo"] - ).toEqual( - "FRA" - ) + expect(leftFencer["teamInfo"]).toEqual("FRA") }); - test('team id', () => { - expect( - leftFencer["teamId"] - ).toEqual( - "leftTeamId" - ) + expect(leftFencer["teamId"]).toEqual("leftTeamId") }); - test('team Member Name', () => { - expect( - leftFencer["teamMemberName"] - ).toEqual( - "leftTeamMemberName" - ) + expect(leftFencer["teamMemberName"]).toEqual("leftTeamMemberName") }); }); }); }) + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[DISP_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with all 19 elements (command + 18 params)', () => { + const result = build({ + piste: 'RED', eventId: '24', competitionCode: 'EIM', competitionPhase: 'T32', + boutOrderInPhase: '1', boutId: '32', beginTime: '14:45', stopwatch: '3:00', + rightFencer: { id: '33', name: ' IVANOV Sidor', teamInfo: 'CAN', teamId: 'rightTeamId', teamMemberName: 'rightTeamMemberName' }, + leftFencer: { id: '531', name: 'LIMON Jua', teamInfo: 'FRA', teamId: 'leftTeamId', teamMemberName: 'leftTeamMemberName' }, + }); + expect(result).toEqual([DISP_COMMAND, ...EXAMPLE_DISP_TOKENS]); + }); +}); diff --git a/tests/commands/getteam.test.js b/tests/commands/getteam.test.js new file mode 100644 index 0000000..60208ee --- /dev/null +++ b/tests/commands/getteam.test.js @@ -0,0 +1,54 @@ +import {GETTEAM_COMMAND, register, registerBuilder, build} from "../../src/commands/getteam"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[GETTEAM_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[GETTEAM_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI"]); }) + .toThrowError(`Incompatible command tokens for >${GETTEAM_COMMAND}<. Expected 2, Got: 1`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${GETTEAM_COMMAND}<. Expected 2, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['RED', 'LEFT']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(GETTEAM_COMMAND); + }); + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('RED'); + }); + test('reads the side', () => { + expect(parsedResult['side']).toEqual('LEFT'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[GETTEAM_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command, piste and side', () => { + expect(build({ piste: 'RED', side: 'LEFT' })).toEqual(['GETTEAM', 'RED', 'LEFT']); + }); +}); diff --git a/tests/commands/hello.test.js b/tests/commands/hello.test.js index 13da9dc..28f6a4c 100644 --- a/tests/commands/hello.test.js +++ b/tests/commands/hello.test.js @@ -1,4 +1,4 @@ -import {HELLO_COMMAND, register} from "../../src/commands/hello"; +import {HELLO_COMMAND, register, registerBuilder, build} from "../../src/commands/hello"; const EXAMPLE_DISP_TOKENS = [ 'RED' @@ -88,3 +88,29 @@ describe('#parse', () => { }); }); }); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[HELLO_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + describe('with piste code', () => { + test('returns token array with command and piste code', () => { + expect(build({ pisteCode: 'RED' })).toEqual(['HELLO', 'RED']); + }); + }); + + describe('without piste code', () => { + test('returns token array with command only', () => { + expect(build({ pisteCode: '' })).toEqual(['HELLO']); + }); + + test('returns token array with command only when pisteCode is undefined', () => { + expect(build({})).toEqual(['HELLO']); + }); + }); +}); diff --git a/tests/commands/info.test.js b/tests/commands/info.test.js new file mode 100644 index 0000000..4648380 --- /dev/null +++ b/tests/commands/info.test.js @@ -0,0 +1,193 @@ +import {INFO_COMMAND, register, registerBuilder, build} from "../../src/commands/info"; + +const EXAMPLE_INFO_TOKENS = [ + 'RED', // piste + '24', // eventId + 'EIM', // competitionCode + 'T32', // competitionPhase + '1', // boutOrderInPhase + '1', // roundNumber + '32', // boutId + '14:45', // beginTime + '3:00', // stopwatch + 'F', // state (Fencing) + '0', // refereeRemoteControl + 'N', // priority + '0', // callTechnician + '0', // callVideoEngineer + '0', // callDoctor + '0', // callDT + '0', // reverse + '0', // standby + '33', // right id + 'IVANOV Sidor', // right name + 'RUS', // right teamInfo + '', // right teamMemberId + '', // right teamMemberName + '5', // right score + '0', // right yellowCard + '0', // right redCards + '0', // right blackCard + '0', // right usedVideo + '1', // right lamp + '0', // right whiteLamp + '531', // left id + 'LIMON Jua', // left name + 'FRA', // left teamInfo + '', // left teamMemberId + '', // left teamMemberName + '3', // left score + '1', // left yellowCard + '0', // left redCards + '0', // left blackCard + '1', // left usedVideo + '0', // left lamp + '0', // left whiteLamp +]; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[INFO_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[INFO_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI", "THERE"]); }) + .toThrowError(`Incompatible command tokens for >${INFO_COMMAND}<. Expected 42, Got: 2`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${INFO_COMMAND}<. Expected 42, Got: 0`); + }); + }); + + describe("valid tokens", () => { + beforeEach(() => { + parsedResult = parse([...EXAMPLE_INFO_TOKENS]); + }); + + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(INFO_COMMAND); + }); + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('RED'); + }); + test('reads the event id', () => { + expect(parsedResult['eventId']).toEqual('24'); + }); + test('reads the competition code', () => { + expect(parsedResult['competitionCode']).toEqual('EIM'); + }); + test('reads the competition phase', () => { + expect(parsedResult['competitionPhase']).toEqual('T32'); + }); + test('reads the bout order in phase', () => { + expect(parsedResult['boutOrderInPhase']).toEqual('1'); + }); + test('reads the round number', () => { + expect(parsedResult['roundNumber']).toEqual('1'); + }); + test('reads the bout id', () => { + expect(parsedResult['boutId']).toEqual('32'); + }); + test('reads the begin time', () => { + expect(parsedResult['beginTime']).toEqual('14:45'); + }); + test('reads the stopwatch', () => { + expect(parsedResult['stopwatch']).toEqual('3:00'); + }); + test('reads the state', () => { + expect(parsedResult['state']).toEqual('F'); + }); + test('reads the referee remote control', () => { + expect(parsedResult['refereeRemoteControl']).toEqual('0'); + }); + test('reads the priority', () => { + expect(parsedResult['priority']).toEqual('N'); + }); + test('reads call technician', () => { + expect(parsedResult['callTechnician']).toEqual('0'); + }); + test('reads call video engineer', () => { + expect(parsedResult['callVideoEngineer']).toEqual('0'); + }); + test('reads call doctor', () => { + expect(parsedResult['callDoctor']).toEqual('0'); + }); + test('reads call DT', () => { + expect(parsedResult['callDT']).toEqual('0'); + }); + test('reads reverse', () => { + expect(parsedResult['reverse']).toEqual('0'); + }); + test('reads standby', () => { + expect(parsedResult['standby']).toEqual('0'); + }); + + describe('right fencer', () => { + let rightFencer = null; + beforeEach(() => { rightFencer = parsedResult['rightFencer']; }); + + test('id', () => { expect(rightFencer['id']).toEqual('33'); }); + test('name', () => { expect(rightFencer['name']).toEqual('IVANOV Sidor'); }); + test('team info', () => { expect(rightFencer['teamInfo']).toEqual('RUS'); }); + test('team member id', () => { expect(rightFencer['teamMemberId']).toEqual(''); }); + test('team member name', () => { expect(rightFencer['teamMemberName']).toEqual(''); }); + test('score', () => { expect(rightFencer['score']).toEqual('5'); }); + test('yellow card', () => { expect(rightFencer['yellowCard']).toEqual('0'); }); + test('red cards', () => { expect(rightFencer['redCards']).toEqual('0'); }); + test('black card', () => { expect(rightFencer['blackCard']).toEqual('0'); }); + test('used video', () => { expect(rightFencer['usedVideo']).toEqual('0'); }); + test('lamp', () => { expect(rightFencer['lamp']).toEqual('1'); }); + test('white lamp', () => { expect(rightFencer['whiteLamp']).toEqual('0'); }); + }); + + describe('left fencer', () => { + let leftFencer = null; + beforeEach(() => { leftFencer = parsedResult['leftFencer']; }); + + test('id', () => { expect(leftFencer['id']).toEqual('531'); }); + test('name', () => { expect(leftFencer['name']).toEqual('LIMON Jua'); }); + test('team info', () => { expect(leftFencer['teamInfo']).toEqual('FRA'); }); + test('team member id', () => { expect(leftFencer['teamMemberId']).toEqual(''); }); + test('team member name', () => { expect(leftFencer['teamMemberName']).toEqual(''); }); + test('score', () => { expect(leftFencer['score']).toEqual('3'); }); + test('yellow card', () => { expect(leftFencer['yellowCard']).toEqual('1'); }); + test('red cards', () => { expect(leftFencer['redCards']).toEqual('0'); }); + test('black card', () => { expect(leftFencer['blackCard']).toEqual('0'); }); + test('used video', () => { expect(leftFencer['usedVideo']).toEqual('1'); }); + test('lamp', () => { expect(leftFencer['lamp']).toEqual('0'); }); + test('white lamp', () => { expect(leftFencer['whiteLamp']).toEqual('0'); }); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[INFO_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with all 43 elements (command + 42 params)', () => { + const result = build({ + piste: 'RED', eventId: '24', competitionCode: 'EIM', competitionPhase: 'T32', + boutOrderInPhase: '1', roundNumber: '1', boutId: '32', beginTime: '14:45', stopwatch: '3:00', + state: 'F', refereeRemoteControl: '0', priority: 'N', + callTechnician: '0', callVideoEngineer: '0', callDoctor: '0', callDT: '0', + reverse: '0', standby: '0', + rightFencer: { id: '33', name: 'IVANOV Sidor', teamInfo: 'RUS', teamMemberId: '', teamMemberName: '', score: '5', yellowCard: '0', redCards: '0', blackCard: '0', usedVideo: '0', lamp: '1', whiteLamp: '0' }, + leftFencer: { id: '531', name: 'LIMON Jua', teamInfo: 'FRA', teamMemberId: '', teamMemberName: '', score: '3', yellowCard: '1', redCards: '0', blackCard: '0', usedVideo: '1', lamp: '0', whiteLamp: '0' }, + }); + expect(result).toEqual([INFO_COMMAND, ...EXAMPLE_INFO_TOKENS]); + }); +}); diff --git a/tests/commands/msg.test.js b/tests/commands/msg.test.js index 6076fa7..f197c73 100644 --- a/tests/commands/msg.test.js +++ b/tests/commands/msg.test.js @@ -1,5 +1,4 @@ -import {MSG_COMMAND, register} from "../../src/commands/msg"; -import {DISP_COMMAND} from "../../src/commands/disp"; +import {MSG_COMMAND, register, registerBuilder, build} from "../../src/commands/msg"; const EXAMPLE_MSG_TOKENS = [ 'RED', 'Red table come home' @@ -73,3 +72,17 @@ describe('#parse', () => { }); }); }); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[MSG_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command, piste and message', () => { + expect(build({ piste: 'BLUE', message: 'Glove missing' })).toEqual(['MSG', 'BLUE', 'Glove missing']); + }); +}); diff --git a/tests/commands/nak.test.js b/tests/commands/nak.test.js new file mode 100644 index 0000000..f53f1db --- /dev/null +++ b/tests/commands/nak.test.js @@ -0,0 +1,47 @@ +import {NAK_COMMAND, register, registerBuilder, build} from "../../src/commands/nak"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[NAK_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[NAK_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI"]); }) + .toThrowError(`Incompatible command tokens for >${NAK_COMMAND}<. Expected 0, Got: 1`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }).not.toThrow(); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse([]); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(NAK_COMMAND); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[NAK_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command only', () => { + expect(build({})).toEqual(['NAK']); + }); +}); diff --git a/tests/commands/next.test.js b/tests/commands/next.test.js new file mode 100644 index 0000000..76a9f53 --- /dev/null +++ b/tests/commands/next.test.js @@ -0,0 +1,51 @@ +import {NEXT_COMMAND, register, registerBuilder, build} from "../../src/commands/next"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[NEXT_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[NEXT_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI", "THERE"]); }) + .toThrowError(`Incompatible command tokens for >${NEXT_COMMAND}<. Expected 1, Got: 2`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${NEXT_COMMAND}<. Expected 1, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['RED']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(NEXT_COMMAND); + }); + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('RED'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[NEXT_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command and piste', () => { + expect(build({ piste: 'RED' })).toEqual(['NEXT', 'RED']); + }); +}); diff --git a/tests/commands/ping.test.js b/tests/commands/ping.test.js index c802964..3dfc245 100644 --- a/tests/commands/ping.test.js +++ b/tests/commands/ping.test.js @@ -1,15 +1,10 @@ -import {PING_COMMAND, register} from "../../src/commands/ping"; +import {PING_COMMAND, register, registerBuilder, build} from "../../src/commands/ping"; describe('#register', () => { test('basic registration should work', () => { const commandDictionary = {}; register(commandDictionary); - const result = commandDictionary[PING_COMMAND]; - expect( - typeof result - ).toEqual( - 'function' - ); + expect(typeof commandDictionary[PING_COMMAND]).toEqual('function'); }); }); @@ -18,38 +13,37 @@ describe('#parse', () => { let parsedResult = null; describe('invalid length of tokens', function () { - test('problem with token length for this parser', () => { - expect( - () => { - parse(["HI", "THERE", "MONKEY"]); - } - ).toThrowError(`Incompatible command tokens for >${PING_COMMAND}<. Expected 0, Got: 3`); + expect(() => { parse(["HI", "THERE", "MONKEY"]); }) + .toThrowError(`Incompatible command tokens for >${PING_COMMAND}<. Expected 0, Got: 3`); }); - - test('problem with token length for this parser', () => { - expect( - () => { - parse(null); - } - ).not.toThrowError("Anything"); + test('no issue with null tokens', () => { + expect(() => { parse(null); }).not.toThrowError("Anything"); }); }); describe("valid parameters", () => { describe("ping command with no parameter", () => { beforeEach(() => { - const tokens = []; - parsedResult = parse(tokens); + parsedResult = parse([]); }); - test('has the command in the result', () => { - expect( - parsedResult['command'] - ).toEqual( - PING_COMMAND - ) + expect(parsedResult['command']).toEqual(PING_COMMAND); }); }); }); }); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[PING_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command only', () => { + expect(build({})).toEqual(['PING']); + }); +}); diff --git a/tests/commands/prev.test.js b/tests/commands/prev.test.js new file mode 100644 index 0000000..0b8ecf5 --- /dev/null +++ b/tests/commands/prev.test.js @@ -0,0 +1,51 @@ +import {PREV_COMMAND, register, registerBuilder, build} from "../../src/commands/prev"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[PREV_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[PREV_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI", "THERE"]); }) + .toThrowError(`Incompatible command tokens for >${PREV_COMMAND}<. Expected 1, Got: 2`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${PREV_COMMAND}<. Expected 1, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['1']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(PREV_COMMAND); + }); + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('1'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[PREV_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command and piste', () => { + expect(build({ piste: '1' })).toEqual(['PREV', '1']); + }); +}); diff --git a/tests/commands/replace.test.js b/tests/commands/replace.test.js new file mode 100644 index 0000000..bc33e09 --- /dev/null +++ b/tests/commands/replace.test.js @@ -0,0 +1,98 @@ +import {REPLACE_COMMAND, register, registerBuilder, build} from "../../src/commands/replace"; + +const EXAMPLE_REPLACE_TOKENS = [ + 'RED', 'LEFT', '1' +]; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + const result = commandDictionary[REPLACE_COMMAND]; + expect( + typeof result + ).toEqual( + 'function' + ); + }); +}); + +describe('#parse', () => { + const parse = register({})[REPLACE_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + + test('problem with token length for this parser', () => { + expect( + () => { + parse(["HI", "THERE"]); + } + ).toThrowError(`Incompatible command tokens for >${REPLACE_COMMAND}<. Expected 3, Got: 2`); + }); + + test('no issue with null tokens', () => { + expect( + () => { + parse(null); + } + ).toThrowError(`Incompatible command tokens for >${REPLACE_COMMAND}<. Expected 3, Got: 0`); + }); + }); + + describe("valid parameters", () => { + describe("replace command with piste, side, and fencer number", () => { + beforeEach(() => { + const tokens = [...EXAMPLE_REPLACE_TOKENS]; + parsedResult = parse(tokens); + }); + + test('has the command in the result', () => { + expect( + parsedResult['command'] + ).toEqual( + REPLACE_COMMAND + ) + }); + + test('reads the piste', () => { + expect( + parsedResult['piste'] + ).toEqual( + 'RED' + ) + }); + + test('reads the side', () => { + expect( + parsedResult['side'] + ).toEqual( + 'LEFT' + ) + }); + + test('reads the fencer number', () => { + expect( + parsedResult['fencerNumber'] + ).toEqual( + '1' + ) + }); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[REPLACE_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command, piste, side and fencerNumber', () => { + expect(build({ piste: 'RED', side: 'LEFT', fencerNumber: '1' })) + .toEqual(['REPLACE', 'RED', 'LEFT', '1']); + }); +}); diff --git a/tests/commands/standby.test.js b/tests/commands/standby.test.js new file mode 100644 index 0000000..7baadf1 --- /dev/null +++ b/tests/commands/standby.test.js @@ -0,0 +1,51 @@ +import {STANDBY_COMMAND, register, registerBuilder, build} from "../../src/commands/standby"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[STANDBY_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[STANDBY_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI", "THERE"]); }) + .toThrowError(`Incompatible command tokens for >${STANDBY_COMMAND}<. Expected 1, Got: 2`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${STANDBY_COMMAND}<. Expected 1, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['FINAL']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(STANDBY_COMMAND); + }); + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('FINAL'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[STANDBY_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command and piste', () => { + expect(build({ piste: 'FINAL' })).toEqual(['STANDBY', 'FINAL']); + }); +}); diff --git a/tests/commands/stop.test.js b/tests/commands/stop.test.js index 4cb5e3d..d67bfe1 100644 --- a/tests/commands/stop.test.js +++ b/tests/commands/stop.test.js @@ -1,15 +1,10 @@ -import {STOP_COMMAND, register} from "../../src/commands/stop"; +import {STOP_COMMAND, register, registerBuilder, build} from "../../src/commands/stop"; describe('#register', () => { test('basic registration should work', () => { const commandDictionary = {}; register(commandDictionary); - const result = commandDictionary[STOP_COMMAND]; - expect( - typeof result - ).toEqual( - 'function' - ); + expect(typeof commandDictionary[STOP_COMMAND]).toEqual('function'); }); }); @@ -18,38 +13,37 @@ describe('#parse', () => { let parsedResult = null; describe('invalid length of tokens', function () { - test('problem with token length for this parser', () => { - expect( - () => { - parse(["HI", "THERE", "MONKEY"]); - } - ).toThrowError(`Incompatible command tokens for >${STOP_COMMAND}<. Expected 0, Got: 3`); + expect(() => { parse(["HI", "THERE", "MONKEY"]); }) + .toThrowError(`Incompatible command tokens for >${STOP_COMMAND}<. Expected 0, Got: 3`); }); - test('no issue with token length for this parser', () => { - expect( - () => { - parse(null); - } - ).not.toThrowError("Anything"); + expect(() => { parse(null); }).not.toThrowError("Anything"); }); }); describe("valid parameters", () => { describe("stop command with no parameter", () => { beforeEach(() => { - const tokens = []; - parsedResult = parse(tokens); + parsedResult = parse([]); }); - test('has the command in the result', () => { - expect( - parsedResult['command'] - ).toEqual( - STOP_COMMAND - ) + expect(parsedResult['command']).toEqual(STOP_COMMAND); }); }); }); }); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[STOP_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command only', () => { + expect(build({})).toEqual(['STOP']); + }); +}); diff --git a/tests/commands/team.test.js b/tests/commands/team.test.js new file mode 100644 index 0000000..832b760 --- /dev/null +++ b/tests/commands/team.test.js @@ -0,0 +1,136 @@ +import {TEAM_COMMAND, register, registerBuilder, build} from "../../src/commands/team"; + +const EXAMPLE_TEAM_TOKENS = [ + 'RED', 'LEFT', + '234', 'IVANOV Fedor', + '542', 'PETROV Ivan', + '43', 'SIDOROV Evgeny', + '2', 'OH Semen', + '1', '2', '3', '2', '1', '3', '3', '1', '2', + '435533' +]; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + const result = commandDictionary[TEAM_COMMAND]; + expect( + typeof result + ).toEqual( + 'function' + ); + }); +}); + +describe('#parse', () => { + const parse = register({})[TEAM_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + + test('problem with token length for this parser', () => { + expect( + () => { + parse(["HI", "THERE"]); + } + ).toThrowError(`Incompatible command tokens for >${TEAM_COMMAND}<. Expected 20, Got: 2`); + }); + + test('no issue with null tokens', () => { + expect( + () => { + parse(null); + } + ).toThrowError(`Incompatible command tokens for >${TEAM_COMMAND}<. Expected 20, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + const tokens = [...EXAMPLE_TEAM_TOKENS]; + parsedResult = parse(tokens); + }); + + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(TEAM_COMMAND); + }); + + test('reads the piste', () => { + expect(parsedResult['piste']).toEqual('RED'); + }); + + test('reads the side', () => { + expect(parsedResult['side']).toEqual('LEFT'); + }); + + describe('members', () => { + test('member 1 id', () => { + expect(parsedResult['members'][0]['id']).toEqual('234'); + }); + test('member 1 name', () => { + expect(parsedResult['members'][0]['name']).toEqual('IVANOV Fedor'); + }); + test('member 2 id', () => { + expect(parsedResult['members'][1]['id']).toEqual('542'); + }); + test('member 2 name', () => { + expect(parsedResult['members'][1]['name']).toEqual('PETROV Ivan'); + }); + test('member 3 id', () => { + expect(parsedResult['members'][2]['id']).toEqual('43'); + }); + test('member 3 name', () => { + expect(parsedResult['members'][2]['name']).toEqual('SIDOROV Evgeny'); + }); + test('reserve id', () => { + expect(parsedResult['members'][3]['id']).toEqual('2'); + }); + test('reserve name', () => { + expect(parsedResult['members'][3]['name']).toEqual('OH Semen'); + }); + }); + + describe('rounds', () => { + test('has 9 round assignments', () => { + expect(parsedResult['rounds'].length).toEqual(9); + }); + test('round 1', () => { + expect(parsedResult['rounds'][0]).toEqual('1'); + }); + test('round 9', () => { + expect(parsedResult['rounds'][8]).toEqual('2'); + }); + }); + + test('reads the unique id', () => { + expect(parsedResult['uniqueId']).toEqual('435533'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[TEAM_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with all 21 elements (command + 20 params)', () => { + const result = build({ + piste: 'RED', + side: 'LEFT', + members: [ + { id: '234', name: 'IVANOV Fedor' }, + { id: '542', name: 'PETROV Ivan' }, + { id: '43', name: 'SIDOROV Evgeny' }, + { id: '2', name: 'OH Semen' }, + ], + rounds: ['1', '2', '3', '2', '1', '3', '3', '1', '2'], + uniqueId: '435533', + }); + expect(result).toEqual([TEAM_COMMAND, ...EXAMPLE_TEAM_TOKENS]); + }); +}); diff --git a/tests/commands/updated.test.js b/tests/commands/updated.test.js new file mode 100644 index 0000000..0644316 --- /dev/null +++ b/tests/commands/updated.test.js @@ -0,0 +1,54 @@ +import {UPDATED_COMMAND, register, registerBuilder, build} from "../../src/commands/updated"; + +describe('#register', () => { + test('basic registration should work', () => { + const commandDictionary = {}; + register(commandDictionary); + expect(typeof commandDictionary[UPDATED_COMMAND]).toEqual('function'); + }); +}); + +describe('#parse', () => { + const parse = register({})[UPDATED_COMMAND]; + let parsedResult = null; + + describe('invalid length of tokens', function () { + test('problem with token length for this parser', () => { + expect(() => { parse(["HI"]); }) + .toThrowError(`Incompatible command tokens for >${UPDATED_COMMAND}<. Expected 2, Got: 1`); + }); + test('no issue with null tokens', () => { + expect(() => { parse(null); }) + .toThrowError(`Incompatible command tokens for >${UPDATED_COMMAND}<. Expected 2, Got: 0`); + }); + }); + + describe("valid parameters", () => { + beforeEach(() => { + parsedResult = parse(['23', 'EIM']); + }); + test('has the command in the result', () => { + expect(parsedResult['command']).toEqual(UPDATED_COMMAND); + }); + test('reads the event id', () => { + expect(parsedResult['eventId']).toEqual('23'); + }); + test('reads the competition code', () => { + expect(parsedResult['competitionCode']).toEqual('EIM'); + }); + }); +}); + +describe('#registerBuilder', () => { + test('basic registration should work', () => { + const builderDictionary = {}; + registerBuilder(builderDictionary); + expect(typeof builderDictionary[UPDATED_COMMAND]).toEqual('function'); + }); +}); + +describe('#build', () => { + test('returns token array with command, eventId and competitionCode', () => { + expect(build({ eventId: '23', competitionCode: 'EIM' })).toEqual(['UPDATED', '23', 'EIM']); + }); +}); diff --git a/tests/protocol/cyrano.test.js b/tests/protocol/cyrano.test.js index be81032..95d984d 100644 --- a/tests/protocol/cyrano.test.js +++ b/tests/protocol/cyrano.test.js @@ -1,79 +1,244 @@ -import {process} from "../../src/protocol/cyrano"; +import {process, compose} from "../../src/protocol/cyrano"; +import {ACK_COMMAND} from "../../src/commands/ack"; +import {BOUTSTOP_COMMAND} from "../../src/commands/boutstop"; +import {BROKEN_COMMAND} from "../../src/commands/broken"; +import {DENY_COMMAND} from "../../src/commands/deny"; import {DISP_COMMAND} from "../../src/commands/disp"; +import {GETTEAM_COMMAND} from "../../src/commands/getteam"; import {HELLO_COMMAND} from "../../src/commands/hello"; +import {INFO_COMMAND} from "../../src/commands/info"; +import {MSG_COMMAND} from "../../src/commands/msg"; +import {NAK_COMMAND} from "../../src/commands/nak"; +import {NEXT_COMMAND} from "../../src/commands/next"; import {PING_COMMAND} from "../../src/commands/ping"; +import {PREV_COMMAND} from "../../src/commands/prev"; +import {REPLACE_COMMAND} from "../../src/commands/replace"; +import {STANDBY_COMMAND} from "../../src/commands/standby"; import {STOP_COMMAND} from "../../src/commands/stop"; -import {MSG_COMMAND} from "../../src/commands/msg"; +import {TEAM_COMMAND} from "../../src/commands/team"; +import {UPDATED_COMMAND} from "../../src/commands/updated"; -const DISP_COMMAND_EXAMPLE = "|EFP2|DISP|RED|24|EIM|T32|1|32|14:45|3:00|33| IVANOV Sidor|CAN|||531|LIMON Jua|FRA|||"; -const HELLO_COMMAND_EXAMPLE = "|EFP2|HELLO|RED|"; -const PING_COMMAND_EXAMPLE = "|EFP2|PING|"; -const STOP_COMMAND_EXAMPLE = "|EFP2|STOP|"; -const MSG_COMMAND_EXAMPLE = "|EFP2|MSG|BLUE|Glove missing|"; +const ACK_EXAMPLE = "|EFP2|ACK|"; +const BOUTSTOP_EXAMPLE = "|EFP2|BOUTSTOP|RED|"; +const BROKEN_EXAMPLE = "|EFP2|BROKEN|3|"; +const DENY_EXAMPLE = "|EFP2|DENY|Piste does not exists|"; +const DISP_EXAMPLE = "|EFP2|DISP|RED|24|EIM|T32|1|32|14:45|3:00|33| IVANOV Sidor|CAN|||531|LIMON Jua|FRA|||"; +const GETTEAM_EXAMPLE = "|EFP2|GETTEAM|RED|LEFT|"; +const HELLO_EXAMPLE = "|EFP2|HELLO|RED|"; +const INFO_EXAMPLE = "|EFP2|INFO|RED|24|EIM|T32|1|1|32|14:45|3:00|F|0|N|0|0|0|0|0|0|33|IVANOV Sidor|RUS|||5|0|0|0|0|1|0|531|LIMON Jua|FRA|||3|1|0|0|1|0|0|"; +const MSG_EXAMPLE = "|EFP2|MSG|BLUE|Glove missing|"; +const NAK_EXAMPLE = "|EFP2|NAK|"; +const NEXT_EXAMPLE = "|EFP2|NEXT|RED|"; +const PING_EXAMPLE = "|EFP2|PING|"; +const PREV_EXAMPLE = "|EFP2|PREV|1|"; +const REPLACE_EXAMPLE = "|EFP2|REPLACE|RED|LEFT|1|"; +const STANDBY_EXAMPLE = "|EFP2|STANDBY|FINAL|"; +const STOP_EXAMPLE = "|EFP2|STOP|"; +const TEAM_EXAMPLE = "|EFP2|TEAM|RED|LEFT|234|IVANOV Fedor|542|PETROV Ivan|43|SIDOROV Evgeny|2|OH Semen|1|2|3|2|1|3|3|1|2|435533|"; +const UPDATED_EXAMPLE = "|EFP2|UPDATED|23|EIM|"; describe('#process', () => { test('unsupported protocol', () => { expect( - () => { - process("|JAMES|DISP|NOT RELEVANT"); - } + () => { process("|JAMES|DISP|NOT RELEVANT"); } ).toThrowError("Unsupported protocol >JAMES<"); }); - test('extract command name', () => { + test('unknown command throws', () => { expect( - process(DISP_COMMAND_EXAMPLE)["command"] - ).toEqual( - DISP_COMMAND - ); + () => { process("|EFP2|FOOBAR|"); } + ).toThrow(); }); - test('extract command name for hello', () => { - expect( - process(HELLO_COMMAND_EXAMPLE)["command"] - ).toEqual( - HELLO_COMMAND - ); + test('extract command name for ACK', () => { + expect(process(ACK_EXAMPLE)["command"]).toEqual(ACK_COMMAND); }); - test('extract command name for ping', () => { - expect( - process(PING_COMMAND_EXAMPLE)["command"] - ).toEqual( - PING_COMMAND - ); + test('extract command name for BOUTSTOP', () => { + expect(process(BOUTSTOP_EXAMPLE)["command"]).toEqual(BOUTSTOP_COMMAND); }); - test('extract command name for ping', () => { - expect( - process(STOP_COMMAND_EXAMPLE)["command"] - ).toEqual( - STOP_COMMAND - ); + test('extract command name for BROKEN', () => { + expect(process(BROKEN_EXAMPLE)["command"]).toEqual(BROKEN_COMMAND); }); - test('extract command name for ping', () => { - expect( - process(MSG_COMMAND_EXAMPLE)["command"] - ).toEqual( - MSG_COMMAND - ); + test('extract command name for DENY', () => { + expect(process(DENY_EXAMPLE)["command"]).toEqual(DENY_COMMAND); }); - test.skip('test display function', () => { - expect( - process(DISP_COMMAND_EXAMPLE) - ).toEqual( - { - "command": "DISP", - "piste": "RED", - "eventId": "24", - "competitionCode": "EIM", - "competitionPhase": "T32", - "red": { - "fencer": "bob" - } - } - ); + test('extract command name for DISP', () => { + expect(process(DISP_EXAMPLE)["command"]).toEqual(DISP_COMMAND); + }); + + test('extract command name for GETTEAM', () => { + expect(process(GETTEAM_EXAMPLE)["command"]).toEqual(GETTEAM_COMMAND); + }); + + test('extract command name for HELLO', () => { + expect(process(HELLO_EXAMPLE)["command"]).toEqual(HELLO_COMMAND); + }); + + test('extract command name for INFO', () => { + expect(process(INFO_EXAMPLE)["command"]).toEqual(INFO_COMMAND); + }); + + test('extract command name for MSG', () => { + expect(process(MSG_EXAMPLE)["command"]).toEqual(MSG_COMMAND); + }); + + test('extract command name for NAK', () => { + expect(process(NAK_EXAMPLE)["command"]).toEqual(NAK_COMMAND); + }); + + test('extract command name for NEXT', () => { + expect(process(NEXT_EXAMPLE)["command"]).toEqual(NEXT_COMMAND); + }); + + test('extract command name for PING', () => { + expect(process(PING_EXAMPLE)["command"]).toEqual(PING_COMMAND); + }); + + test('extract command name for PREV', () => { + expect(process(PREV_EXAMPLE)["command"]).toEqual(PREV_COMMAND); + }); + + test('extract command name for REPLACE', () => { + expect(process(REPLACE_EXAMPLE)["command"]).toEqual(REPLACE_COMMAND); + }); + + test('extract command name for STANDBY', () => { + expect(process(STANDBY_EXAMPLE)["command"]).toEqual(STANDBY_COMMAND); + }); + + test('extract command name for STOP', () => { + expect(process(STOP_EXAMPLE)["command"]).toEqual(STOP_COMMAND); + }); + + test('extract command name for TEAM', () => { + expect(process(TEAM_EXAMPLE)["command"]).toEqual(TEAM_COMMAND); + }); + + test('extract command name for UPDATED', () => { + expect(process(UPDATED_EXAMPLE)["command"]).toEqual(UPDATED_COMMAND); + }); +}); + +describe('#compose', () => { + test('throws for unknown command', () => { + expect(() => { compose({ command: 'FOOBAR' }); }) + .toThrowError("No builder registered for command >FOOBAR<"); + }); + + describe('ACK round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(ACK_EXAMPLE))).toEqual(ACK_EXAMPLE); + }); + }); + + describe('BOUTSTOP round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(BOUTSTOP_EXAMPLE))).toEqual(BOUTSTOP_EXAMPLE); + }); + }); + + describe('BROKEN round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(BROKEN_EXAMPLE))).toEqual(BROKEN_EXAMPLE); + }); + }); + + describe('DENY round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(DENY_EXAMPLE))).toEqual(DENY_EXAMPLE); + }); + }); + + describe('DISP round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(DISP_EXAMPLE))).toEqual(DISP_EXAMPLE); + }); + }); + + describe('GETTEAM round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(GETTEAM_EXAMPLE))).toEqual(GETTEAM_EXAMPLE); + }); + }); + + describe('HELLO round-trip', () => { + test('with piste code', () => { + expect(compose(process(HELLO_EXAMPLE))).toEqual(HELLO_EXAMPLE); + }); + + test('without piste code', () => { + const bare = '|EFP2|HELLO|'; + expect(compose(process(bare))).toEqual(bare); + }); + }); + + describe('INFO round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(INFO_EXAMPLE))).toEqual(INFO_EXAMPLE); + }); + }); + + describe('MSG round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(MSG_EXAMPLE))).toEqual(MSG_EXAMPLE); + }); + }); + + describe('NAK round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(NAK_EXAMPLE))).toEqual(NAK_EXAMPLE); + }); + }); + + describe('NEXT round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(NEXT_EXAMPLE))).toEqual(NEXT_EXAMPLE); + }); + }); + + describe('PING round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(PING_EXAMPLE))).toEqual(PING_EXAMPLE); + }); + }); + + describe('PREV round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(PREV_EXAMPLE))).toEqual(PREV_EXAMPLE); + }); + }); + + describe('REPLACE round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(REPLACE_EXAMPLE))).toEqual(REPLACE_EXAMPLE); + }); + }); + + describe('STANDBY round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(STANDBY_EXAMPLE))).toEqual(STANDBY_EXAMPLE); + }); + }); + + describe('STOP round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(STOP_EXAMPLE))).toEqual(STOP_EXAMPLE); + }); + }); + + describe('TEAM round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(TEAM_EXAMPLE))).toEqual(TEAM_EXAMPLE); + }); + }); + + describe('UPDATED round-trip', () => { + test('round-trips correctly', () => { + expect(compose(process(UPDATED_EXAMPLE))).toEqual(UPDATED_EXAMPLE); + }); }); });