Last active
September 2, 2021 02:42
-
-
Save qnkhuat/f43ea8af79c73b06261b1361e5fd99b7 to your computer and use it in GitHub Desktop.
`node server.js` to run. `node server.js -test` to run test. Default port is 3000
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# PigeonLab Engineering Test | |
## Overview | |
Your task is to create a TCP based calculator service and deploy it. It'll take | |
in simple math operations such as `1+1`, or `5*10` and reply with the answer. | |
You should implement the server in node.js, with no dependancies, and in a single | |
js file. Include any automated tests in the same file. | |
## Input / Output | |
The server should accept input over TCP on a port of your choosing. Input is text | |
based with each command followed by a newline character. Responses are also given | |
as text followed by a newline. | |
For a given connection, the output should have one line of output per line of | |
input. (Don't send human friendly messages on the socket.) | |
Note that TCP connections are streams of bytes, and one 'data' event does not | |
correspond to one line of input. **Your input lines might be split or merged | |
arbitrarily.** Note that many examples of TCP servers that you might find on google | |
do not handle this correctly. It's particularly important to get this right because | |
our testing script relies on it. | |
## Semantics | |
Each command should have the syntax `<number><operator><number>`. | |
Where: | |
* `number` is a decimal representation of an unsigned 32 bit integer. | |
* `operator` is one of `+`, `-`, `*`, `/`, `%`. | |
When an invalid command is recieved, the response should be an error. Commands | |
with extra whitespace are invalid. | |
Math operations should be evaluated according to the semantics of **unsigned 32 bit | |
numbers**. For example, the result of `4294967295+1` is `0`. Rounding for | |
division is towards zero. Division or modulo by zero should generate an error. | |
Errors should be represented like `error: <human readable message>`. Errors take | |
up one line, just like normal responses. After an error, the server should process | |
the next line as normal. | |
### Example Input | |
``` | |
1+1 | |
10*5 | |
101/10 | |
99%10 | |
0-1 | |
0/0 | |
0.1+0.2 | |
1 + 1 | |
hello world | |
``` | |
### Example Output | |
``` | |
2 | |
50 | |
10 | |
9 | |
4294967295 | |
error: division by zero | |
error: incorrect syntax | |
error: incorrect syntax | |
error: incorrect syntax | |
``` | |
## Submission | |
Create a secret gist on https://gist.github.com and email us the link. | |
Please don't make your code public. | |
*/ | |
let net = require('net'); | |
// *** Error code *** // | |
const newError = (msg) => new Error(`error: ${msg}`); | |
const ErrorIncorrectSyntax = newError('incorrect syntax'); | |
const ErrorDivisionByZero = newError('division by zero'); | |
const ErrorModuloByZero = newError('modulo by zero'); | |
// *** Constants ***// | |
const PORT = 3000; | |
const HOST = 'localhost'; | |
const EOL = '\n'; | |
const commandRegex = /^([0-9]+)([\/\+\-\*%])([0-9]+)$/; | |
const opFunc = { | |
'+': (x, y) => x + y, | |
'-': (x, y) => x - y, | |
'*': (x, y) => x * y, | |
'/': (x, y) => { | |
if (y == 0) throw ErrorDivisionByZero; | |
return x / y | |
}, | |
'%': (x, y) => { | |
if (y == 0) throw ErrorModuloByZero; | |
return x / y | |
}, | |
}; | |
// *** Helpers *** // | |
const toUint32 = (x) => x >>> 0; | |
const calc = (x, op, y) => { | |
if (!(op in opFunc)) return ErrorIncorrectSyntax.message; | |
let result = opFunc[op](toUint32(x), toUint32(y)); | |
return toUint32(result); | |
} | |
const handleCommand = (command) => { | |
let matches = command.match(commandRegex); | |
if (matches != null && matches.length == 4) { | |
try{ | |
result = calc(matches[1], matches[2], matches[3]); | |
return result.toString(); | |
} catch(e) { | |
return e.message; | |
} | |
} else { | |
return ErrorIncorrectSyntax.message | |
} | |
} | |
function onConnect(socket){ | |
var line = ""; | |
socket.on('readable', function(){ | |
let reqBuffer = Buffer.from(''); | |
// temporary buffer to read in chunks | |
let buf; | |
// read until reach end of data | |
while(true) { | |
buf = socket.read(); | |
if (buf === null) break; | |
reqBuffer = Buffer.concat([reqBuffer, buf]); | |
} | |
let eolindex = reqBuffer.indexOf(EOL); | |
// not found skip till next request | |
if (eolindex == -1){ | |
line += reqBuffer.toString(); | |
return | |
} | |
// in case a new line is in the middle of reqBuffer | |
if (eolindex != -1 && eolindex != Buffer.byteLength(reqBuffer) - 1) { | |
// push back the remaining bytes to readable stream | |
let remaining = reqBuffer.slice(eolindex + 1, Buffer.byteLength(reqBuffer)); | |
socket.unshift(remaining); | |
reqBuffer = reqBuffer.slice(0, eolindex); | |
} | |
line += reqBuffer.toString(); | |
let command = line.trim(); | |
socket.write(handleCommand(command)); | |
socket.write('\n'); | |
line = ''; // reset line | |
}); | |
//Handle client connection termination. | |
socket.on('close',function(){ | |
console.log(`Terminated the connection`); | |
}); | |
//Handle Client connection error. | |
socket.on('error',function(error){ | |
console.error(`Connection Error ${error}`); | |
}); | |
}; | |
const test = () => { | |
const assertEqual = (a, b, msg) => { | |
if (a == b){ | |
return true | |
} else { | |
console.error(`Expected: ${a}, Got ${b}. ${msg}`); | |
return false | |
} | |
} | |
// func is function to test | |
// each test cases is an array which the first elem is a list of params, the second is the expected outcome | |
const runner = (func, cases) => { | |
let successes = 0; | |
console.log("Testing: ", func.name); | |
cases.forEach((xs) => { | |
let output = func(...xs[0]); | |
successes += assertEqual(output, xs[1], `Args: ${xs[0]}`); | |
}) | |
console.log(`[${successes == cases.length ? "PASS" : "Fail"}] - ${successes}/${cases.length}`); | |
console.log("----------------------") | |
} | |
runner(toUint32, | |
[ | |
[[0], 0], | |
[[-1], 4294967295], | |
[[4294967296], 0], | |
] | |
); | |
runner(calc, | |
[ | |
[[1, "+", 1], 2], | |
[[10, "*", 5], 50], | |
[[101, "/", 10], 10], | |
[[99, "%", 10], 9], | |
[[0, "-", 1], 4294967295], | |
] | |
); | |
runner(handleCommand, | |
[ | |
[["1+1"], "2"], | |
[["10*5"], "50"], | |
[["101/10"], "10"], | |
[["99%10"], "9"], | |
[["0-1"], "4294967295"], | |
[["0:1"], ErrorIncorrectSyntax.message], | |
[["0-1\n"], ErrorIncorrectSyntax.message], | |
[["0/0"], ErrorDivisionByZero.message], | |
[["3%0"], ErrorModuloByZero.message], | |
[["1 + 1"], ErrorIncorrectSyntax.message], | |
[["Hello world"], ErrorIncorrectSyntax.message], | |
] | |
) | |
} | |
var myArgs = process.argv; | |
if (myArgs.length > 1 && myArgs[2] == '-test') { | |
test(); | |
} else { | |
let server = net.createServer(onConnect) | |
console.log(`Listening to: ${HOST}:${PORT}`) | |
server.listen(PORT, HOST); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
node server.js
to run.node server.js -test
to run test.Default port is 3000