diff --git a/.gitignore b/.gitignore index 3e759b7..39f698b 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,91 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +elevator/coverage/clover.xml +elevator/coverage/coverage-final.json +elevator/coverage/lcov.info +elevator/coverage/lcov-report/base.css +elevator/coverage/lcov-report/block-navigation.js +elevator/coverage/lcov-report/favicon.png +elevator/coverage/lcov-report/index.html +elevator/coverage/lcov-report/prettify.css +elevator/coverage/lcov-report/prettify.js +elevator/coverage/lcov-report/sort-arrow-sprite.png +elevator/coverage/lcov-report/sorter.js +elevator/package-lock.json +elevator/.vscode/launch.json +elevator/elevator.code-workspace +elevator/elevator.log.json +elevator/elevator.log.json +elevator/report/display.html +elevator/report/index.html +elevator/report/report.history.js +elevator/report/report.history.json +elevator/report/report.js +elevator/report/report.json +elevator/report/assets/css/morris.css +elevator/report/assets/css/plato-display.css +elevator/report/assets/css/plato-file.css +elevator/report/assets/css/plato-overview.css +elevator/report/assets/css/plato.css +elevator/report/assets/css/vendor/bootstrap.css +elevator/report/assets/css/vendor/codemirror.css +elevator/report/assets/css/vendor/font-awesome.css +elevator/report/assets/css/vendor/morris.css +elevator/report/assets/font/fontawesome-webfont.eot +elevator/report/assets/font/fontawesome-webfont.svg +elevator/report/assets/font/fontawesome-webfont.ttf +elevator/report/assets/font/fontawesome-webfont.woff +elevator/report/assets/scripts/codemirror.markpopovertext.js +elevator/report/assets/scripts/plato-display.js +elevator/report/assets/scripts/plato-file.js +elevator/report/assets/scripts/plato-overview.js +elevator/report/assets/scripts/plato-sortable-file-list.js +elevator/report/assets/scripts/bundles/codemirror.js +elevator/report/assets/scripts/bundles/core-bundle.js +elevator/report/assets/scripts/vendor/bootstrap-popover.js +elevator/report/assets/scripts/vendor/bootstrap-tooltip.js +elevator/report/assets/scripts/vendor/jquery-1.8.3.min.js +elevator/report/assets/scripts/vendor/jquery.fittext.js +elevator/report/assets/scripts/vendor/lodash.min.js +elevator/report/assets/scripts/vendor/morris.min.js +elevator/report/assets/scripts/vendor/raphael-min.js +elevator/report/assets/scripts/vendor/codemirror/codemirror.js +elevator/report/assets/scripts/vendor/codemirror/javascript.js +elevator/report/assets/scripts/vendor/codemirror/util/closetag.js +elevator/report/assets/scripts/vendor/codemirror/util/colorize.js +elevator/report/assets/scripts/vendor/codemirror/util/continuecomment.js +elevator/report/assets/scripts/vendor/codemirror/util/continuelist.js +elevator/report/assets/scripts/vendor/codemirror/util/dialog.css +elevator/report/assets/scripts/vendor/codemirror/util/dialog.js +elevator/report/assets/scripts/vendor/codemirror/util/foldcode.js +elevator/report/assets/scripts/vendor/codemirror/util/formatting.js +elevator/report/assets/scripts/vendor/codemirror/util/javascript-hint.js +elevator/report/assets/scripts/vendor/codemirror/util/loadmode.js +elevator/report/assets/scripts/vendor/codemirror/util/match-highlighter.js +elevator/report/assets/scripts/vendor/codemirror/util/matchbrackets.js +elevator/report/assets/scripts/vendor/codemirror/util/multiplex.js +elevator/report/assets/scripts/vendor/codemirror/util/overlay.js +elevator/report/assets/scripts/vendor/codemirror/util/pig-hint.js +elevator/report/assets/scripts/vendor/codemirror/util/runmode-standalone.js +elevator/report/assets/scripts/vendor/codemirror/util/runmode.js +elevator/report/assets/scripts/vendor/codemirror/util/search.js +elevator/report/assets/scripts/vendor/codemirror/util/searchcursor.js +elevator/report/assets/scripts/vendor/codemirror/util/simple-hint.css +elevator/report/assets/scripts/vendor/codemirror/util/simple-hint.js +elevator/report/assets/scripts/vendor/codemirror/util/xml-hint.js +elevator/report/files/src_elevator_log_js/index.html +elevator/report/files/src_elevator_log_js/report.history.js +elevator/report/files/src_elevator_log_js/report.history.json +elevator/report/files/src_elevator_log_js/report.js +elevator/report/files/src_elevator_log_js/report.json +elevator/report/files/src_elevator_request_js/index.html +elevator/report/files/src_elevator_request_js/report.history.js +elevator/report/files/src_elevator_request_js/report.history.json +elevator/report/files/src_elevator_request_js/report.js +elevator/report/files/src_elevator_request_js/report.json +elevator/report/files/src_elevator_service_js/index.html +elevator/report/files/src_elevator_service_js/report.history.js +elevator/report/files/src_elevator_service_js/report.history.json +elevator/report/files/src_elevator_service_js/report.js +elevator/report/files/src_elevator_service_js/report.json diff --git a/elevator/.vscode/settings.json b/elevator/.vscode/settings.json new file mode 100644 index 0000000..eb7750c --- /dev/null +++ b/elevator/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.configureOnOpen": false +} \ No newline at end of file diff --git a/elevator/build.sh b/elevator/build.sh new file mode 100644 index 0000000..39ce6d4 --- /dev/null +++ b/elevator/build.sh @@ -0,0 +1,15 @@ +npm install +[ $? -eq 0 ] || exit 1 + +npm run lint +[ $? -eq 0 ] || exit 1 + +npm test +[ $? -eq 0 ] || exit 1 + +npm run plato +[ $? -eq 0 ] || exit 1 + +npm run start +[ $? -eq 0 ] || exit 1 + diff --git a/elevator/package.json b/elevator/package.json new file mode 100644 index 0000000..fe80864 --- /dev/null +++ b/elevator/package.json @@ -0,0 +1,73 @@ +{ + "name": "elevator.application", + "version": "1.0.0", + "description": "", + "main": "elevator.service.js", + "scripts": { + "test": "jest --collect-coverage --no-cache", + "lint": "eslint src test", + "plato": "plato -d report -r src", + "start": "node src/elevator.service.js" + }, + "author": "", + "license": "ISC", + "eslintConfig": { + "globals": { + "expect": true, + "Wordle": true + }, + "env": { + "browser": true, + "node": true, + "es6": true, + "mocha": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module" + }, + "rules": { + "eqeqeq": "error", + "strict": "error", + "no-var": "error", + "no-unused-vars": [ + "off" + ], + "prefer-const": "error", + "no-console": "off", + "indent": [ + "error", + 2 + ], + "quotes": [ + "error", + "single", + "avoid-escape" + ], + "semi": [ + "error", + "always" + ] + } + }, + "jest": { + "coveragePathIgnorePatterns": [ + "/src" + ] + }, + "babel": { + "plugins": [ + "@babel/plugin-transform-modules-commonjs" + ] + }, + "devDependencies": { + "@babel/plugin-transform-modules-commonjs": "^7.21.2", + "eslint": "^8.37.0", + "jest": "^29.5.0", + "plato": "^1.7.0" + }, + "dependencies": { + "directory-import": "^2.3.1" + } +} diff --git a/elevator/platocheck.js b/elevator/platocheck.js new file mode 100644 index 0000000..f666f37 --- /dev/null +++ b/elevator/platocheck.js @@ -0,0 +1,9 @@ +require(`${process.cwd()}/report/report`); + +const maintainability = __report.summary.total.maintainability; + +if(maintainability < 70) { + console.log(`Plato reported total maintainability as ${maintainability}`); +} + +process.exitCode = maintainability >= 70 ? 0 : 1; diff --git a/elevator/src/elevator.log.js b/elevator/src/elevator.log.js new file mode 100644 index 0000000..390d8a4 --- /dev/null +++ b/elevator/src/elevator.log.js @@ -0,0 +1,36 @@ +const fs = require('fs'); + +const elevatorLogs = []; + +const typeOfEvent = { + INSIDE: 'inside elevator request', + OUTSIDE: 'outside elevator request', + MOVE: 'move', + STOP: 'stop', +}; + +function getLog() { + return elevatorLogs; +} + +function clearLog() { + elevatorLogs.length = 0; +} + +function logEvent(event, floorNumber, timestamp = new Date().toISOString()) { + elevatorLogs.push({ timestamp, event, floorNumber }); + + return elevatorLogs; +} + +function writeLog() { + const log = JSON.stringify(elevatorLogs, null, 2); + + fs.writeFileSync('./elevator.log.json', log); +} + +module.exports.typeOfEvent = typeOfEvent; +module.exports.writeLog = writeLog; +module.exports.logEvent = logEvent; +module.exports.getLog = getLog; +module.exports.clearLog = clearLog; \ No newline at end of file diff --git a/elevator/src/elevator.request.js b/elevator/src/elevator.request.js new file mode 100644 index 0000000..b602bc8 --- /dev/null +++ b/elevator/src/elevator.request.js @@ -0,0 +1,163 @@ +const elevatorLogging = require('./elevator.log.js'); + +const typeOfEvent = elevatorLogging.typeOfEvent; + +const MAX_FLOOR = 10; +const MIN_FLOOR = 1; +const MOVE_DELAY = 300; +const STOP_DELAY = 100; + +const elevatorSensor = { + currentFloor: 1, + nextFloor: null, + requestedFloor: [], + direction: '', + isMoving: false, +}; + +async function allRequestsCompleted() { + while (elevatorSensor.requestedFloor.length > 0) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + + // Reset elevator position to floor 1 + await resetElevatorPosition(); + + elevatorLogging.writeLog(); + + return true; +} + +function validateFloor(floorNumber) { + if (floorNumber > MAX_FLOOR || floorNumber < MIN_FLOOR) { + throw new Error('Invalid floor number'); + } + return floorNumber; +} + +async function elevatorTimer(time) { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +} + +async function handleRequests() { + while (elevatorSensor.requestedFloor.length > 0) { + if (elevatorSensor.currentFloor < elevatorSensor.nextFloor) { + elevatorSensor.direction = 'U'; + } + + else { + elevatorSensor.direction = 'D'; + } + + elevatorSensor.isMoving = true; + + while (elevatorSensor.nextFloor !== elevatorSensor.currentFloor) { + elevatorSensor.nextFloor = getNextPickup(); + + if (elevatorSensor.direction === 'U') { + elevatorSensor.currentFloor++; + } + + else { + elevatorSensor.currentFloor--; + } + + await elevatorTimer(MOVE_DELAY); + + elevatorLogging.logEvent(typeOfEvent.MOVE, elevatorSensor.currentFloor); + } + + elevatorSensor.isMoving = false; + + await elevatorTimer(STOP_DELAY); + + elevatorLogging.logEvent(typeOfEvent.STOP, elevatorSensor.currentFloor); + + elevatorSensor.requestedFloor.splice(elevatorSensor.requestedFloor.indexOf(elevatorSensor.currentFloor), 1); + + elevatorSensor.nextFloor = getNextPickup(); + } +} + +async function resetElevatorPosition() { + elevatorSensor.requestedFloor = [1]; + + elevatorSensor.nextFloor = 1; + + await handleRequests(); + + elevatorSensor.nextFloor = null; + + elevatorSensor.direction = ''; +} + +function getNextPickup() { + if (elevatorSensor.requestedFloor.length === 0) { + return null; + } + + const floorsInDirection = elevatorSensor.requestedFloor.filter(floor => { + if (elevatorSensor.direction === 'U') { + return floor > elevatorSensor.currentFloor; + } + + else { + return floor < elevatorSensor.currentFloor; + } + }); + + const floorsToConsider = floorsInDirection.length > 0 ? floorsInDirection : elevatorSensor.requestedFloor; + + const distances = floorsToConsider.map(floor => Math.abs(floor - elevatorSensor.currentFloor)); + + const sortedFloors = floorsToConsider.slice().sort((a, b) => distances[floorsToConsider.indexOf(a)] - distances[floorsToConsider.indexOf(b)]); + + elevatorSensor.nextFloor = sortedFloors[0]; + + return sortedFloors[0]; +} + +async function insideElevatorRequest(floorNumber) { + floorNumber = validateFloor(floorNumber); + + elevatorLogging.logEvent(typeOfEvent.INSIDE, floorNumber); + + if (elevatorSensor.requestedFloor.length === 0) { + elevatorSensor.nextFloor = floorNumber; + } + elevatorSensor.requestedFloor.push(floorNumber); + + await (handleRequests()); +} + +async function outsideElevatorRequest(floorNumber) { + validateFloor(floorNumber); + + elevatorLogging.logEvent(typeOfEvent.OUTSIDE, floorNumber); + + if (elevatorSensor.requestedFloor.length === 0) { + elevatorSensor.nextFloor = floorNumber; + } + + if (elevatorSensor.isMoving && Math.abs(floorNumber - elevatorSensor.currentFloor) >= 1 || !elevatorSensor.isMoving) { + elevatorSensor.requestedFloor.push(floorNumber); + } + + else { + elevatorTimer(MOVE_DELAY); + + elevatorSensor.requestedFloor.push(floorNumber); + } + await (handleRequests()); +} + +module.exports.validateFloor = validateFloor; +module.exports.elevatorTimer = elevatorTimer; +module.exports.insideElevatorRequest = insideElevatorRequest; +module.exports.outsideElevatorRequest = outsideElevatorRequest; +module.exports.allRequestsCompleted = allRequestsCompleted; +module.exports.resetElevatorPosition = resetElevatorPosition; diff --git a/elevator/src/elevator.service.js b/elevator/src/elevator.service.js new file mode 100644 index 0000000..ed655a8 --- /dev/null +++ b/elevator/src/elevator.service.js @@ -0,0 +1,52 @@ +const readline = require('readline'); +const elevatorRequest = require('./elevator.request.js'); +let completed = false; + +function handleUserInput(floorNumber) { + console.log('Floor Number:', floorNumber); + + if (floorNumber === 'Q' || floorNumber === 'q') { + console.log('Quitting...'); + + completed = true; + + return; + } + + if (!isNaN(floorNumber)) { + console.log('Inside elevator request'); + + elevatorRequest.insideElevatorRequest(parseInt(floorNumber)); + } + + else { + console.log('Outside elevator request'); + + elevatorRequest.outsideElevatorRequest(parseInt(floorNumber.replace(/\D/g, ''))); + } +} + +const userInterface = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function processUserInput() { + while (completed === false) { + const userInput = await new Promise((resolve) => { + userInterface.question('Please enter console command: ', resolve); + }); + + handleUserInput(userInput); + + } + const allCompleted = await elevatorRequest.allRequestsCompleted(); + + console.log('All requests completed:', allCompleted); + + userInterface.close(); +} + +processUserInput().catch((error) => { + console.error('An error occurred:', error); +}); diff --git a/elevator/test/elevator.log.test.js b/elevator/test/elevator.log.test.js new file mode 100644 index 0000000..a41b08e --- /dev/null +++ b/elevator/test/elevator.log.test.js @@ -0,0 +1,18 @@ +/* eslint-env jest */ + +import { logEvent, writeLog, typeOfEvent } from '../src/elevator.log.js'; + +test('logEvent successfully record an event', () => { + const mockedTimestamp = '2023-08-30T13:00:00.000Z'; + + Date.now = jest.fn(() => new Date(mockedTimestamp).getTime()); + + const log = logEvent(typeOfEvent.INSIDE, 5, mockedTimestamp); + + expect(log).toEqual([{ + 'timestamp':'2023-08-30T13:00:00.000Z', + 'event':'inside elevator request', + 'floorNumber':5 + }]); +}); + diff --git a/elevator/test/elevator.request.test.js b/elevator/test/elevator.request.test.js new file mode 100644 index 0000000..23e0e74 --- /dev/null +++ b/elevator/test/elevator.request.test.js @@ -0,0 +1,106 @@ +/* eslint-env jest */ + +const elevatorLogging = require('../src/elevator.log.js'); +import { resetElevatorPosition, insideElevatorRequest, outsideElevatorRequest, validateFloor, elevatorTimer, allRequestsCompleted } from '../src/elevator.request.js'; +import { typeOfEvent, getLog, clearLog } from '../src/elevator.log.js'; + +test('canary', () => { + expect(true).toBe(true); +}); + +test('validateFloor request returns floor number', () => { + expect(validateFloor(5)).toBe(5); +}); + +test('validateFloor request throws error going past max floor', () => { + expect(() => validateFloor(11)).toThrow('Invalid floor number'); +}); + +test('validateFloor request throws error going past min floor', () => { + expect(() => validateFloor(0)).toThrow('Invalid floor number'); +}); + +test('elevatorTimer given 10ms await 10ms', async () => { + const startTime = Date.now(); + + await elevatorTimer(10); + + const endTime = Date.now(); + + const elapsedSeconds = (endTime - startTime) / 1000; + + expect(elapsedSeconds).toBeCloseTo(.01, 1); +}); + +test('elevatorTimer given 100ms await 100ms', async () => { + const startTime = Date.now(); + + await elevatorTimer(100); + + const endTime = Date.now(); + + const elapsedSeconds = (endTime - startTime) / 1000; + + expect(elapsedSeconds).toBeCloseTo(.1, 1); +}); + +test('elevator made a request inside return correct output', async () => { + await insideElevatorRequest(3); + + const log = getLog(); + + expect(log).toEqual([ + expect.objectContaining({ event: typeOfEvent.INSIDE, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 2 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.STOP, floorNumber: 3 }) + ]); +}); + +test('elevator made a request inside return correct output', async () => { + await resetElevatorPosition(); + + clearLog(); + + await outsideElevatorRequest(3); + + const log = getLog(); + + expect(log).toEqual([ + expect.objectContaining({ event: typeOfEvent.OUTSIDE, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 2 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.STOP, floorNumber: 3 }) + ]); +}); + +test('elevator made a request inside and outside, inside request is processed first', async () => { + await resetElevatorPosition(); + + clearLog(); + + await insideElevatorRequest(3); + + await outsideElevatorRequest(1); + + const log = getLog(); + + expect(log).toEqual([ + expect.objectContaining({ event: typeOfEvent.INSIDE, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 2 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.STOP, floorNumber: 3 }), + expect.objectContaining({ event: typeOfEvent.OUTSIDE, floorNumber: 1 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 2 }), + expect.objectContaining({ event: typeOfEvent.MOVE, floorNumber: 1 }), + expect.objectContaining({ event: typeOfEvent.STOP, floorNumber: 1 }) + ]); +}); + +test('allRequestsCompleted called writeLog', async () => { + const writeLogSpy = jest.spyOn(elevatorLogging, 'writeLog'); + + await allRequestsCompleted(); + + expect(writeLogSpy).toHaveBeenCalled(); +}); diff --git a/elevator/tests.txt b/elevator/tests.txt new file mode 100644 index 0000000..54dc16e --- /dev/null +++ b/elevator/tests.txt @@ -0,0 +1,17 @@ +x canary test + +x validateFloor request to 5th floor return position at 5 +x validateFloor request past max floor throw error +x validateFloor request past min floor throw error + +x elevatorTimer given 10ms await 10ms +x elevatorTimer given 100ms await 100ms + +x allRequestsCompleted called for writeLog + +x elevator made a request inside return correct output +x elevator made a request outside return correct output +x elevator made a request inside then outside return correct output + +x logEvent successfully record an event +