diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2c46f6a..9a0a562 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -96,6 +96,11 @@ jobs: docker-compose ps -a wget --retry-connrefused http://localhost:3000 # /healthcheck + - name: Run scorometer tests + run: | + pip install -r scorometer/requirements.txt + cd scorometer/tests && ./runner.sh + - name: Run robot tests run: | pip install -r back/test/robot/requirements.txt @@ -106,7 +111,7 @@ jobs: name: results path: out - - name: Write results to Pull Request and Summarry + - name: Write results to Pull Request and Summary if: always() && github.event_name == 'pull_request' uses: joonvena/robotframework-reporter-action@v2.1 with: @@ -114,7 +119,7 @@ jobs: gh_access_token: ${{ secrets.GITHUB_TOKEN }} only_summary: false - - name: Write results to Summarry + - name: Write results to Summary if: always() && github.event_name != 'pull_request' uses: joonvena/robotframework-reporter-action@v2.1 with: diff --git a/musics/SCORO_TEST/SCORO_TEST.ini b/musics/SCORO_TEST/SCORO_TEST.ini new file mode 100644 index 0000000..9de9840 --- /dev/null +++ b/musics/SCORO_TEST/SCORO_TEST.ini @@ -0,0 +1,21 @@ +[Metadata] +Name=Symphony No 9 in D Minor +Artist=Beethoven +Genre=Classical +Album=Symphony No 9 + +[Difficulties] +TwoHands=0 +Rhythm=4 +NoteCombo=0 +Arpeggio=6 +Distance=0 +LeftHand=2 +RightHand=1 +LeadHandChange=0 +ChordComplexity=0 +ChordTiming=0 +Length=1 +PedalPoint=0 +Precision=10 + diff --git a/musics/SCORO_TEST/SCORO_TEST.midi b/musics/SCORO_TEST/SCORO_TEST.midi new file mode 100644 index 0000000..58f38bc Binary files /dev/null and b/musics/SCORO_TEST/SCORO_TEST.midi differ diff --git a/musics/SCORO_TEST/SCORO_TEST.midi.bak b/musics/SCORO_TEST/SCORO_TEST.midi.bak new file mode 100644 index 0000000..82ad897 Binary files /dev/null and b/musics/SCORO_TEST/SCORO_TEST.midi.bak differ diff --git a/musics/SCORO_TEST/SCORO_TEST.mxl b/musics/SCORO_TEST/SCORO_TEST.mxl new file mode 100644 index 0000000..bd862dd Binary files /dev/null and b/musics/SCORO_TEST/SCORO_TEST.mxl differ diff --git a/scorometer/chroma_case/Key.py b/scorometer/chroma_case/Key.py index 18a370e..c253bdf 100644 --- a/scorometer/chroma_case/Key.py +++ b/scorometer/chroma_case/Key.py @@ -1,11 +1,9 @@ class Key: - def __init__(self, key: int, start: int, duration: int): - self.key = key - self.start = start - self.duration = duration - self.done = False - - def __repr__(self): - return f"{self.key} ({self.start} - {self.duration})" - + def __init__(self, key: int, start: int, duration: int): + self.key = key + self.start = start + self.duration = duration + self.done = False + def __repr__(self): + return f"{self.key} ({self.start} - {self.duration})" diff --git a/scorometer/chroma_case/Note.py b/scorometer/chroma_case/Note.py index d2c5b37..c16d453 100644 --- a/scorometer/chroma_case/Note.py +++ b/scorometer/chroma_case/Note.py @@ -1,11 +1,11 @@ class Note: - def __init__(self, start_time, data) -> None: + def __init__(self, start_time, data) -> None: - self.__start_time = start_time - self.__data = data + self.__start_time = start_time + self.__data = data - def get_start_time(self): - return self.__start_time - - def get_data(self): - return self.__data + def get_start_time(self): + return self.__start_time + + def get_data(self): + return self.__data diff --git a/scorometer/chroma_case/Partition.py b/scorometer/chroma_case/Partition.py index d3fb497..3a1d0aa 100644 --- a/scorometer/chroma_case/Partition.py +++ b/scorometer/chroma_case/Partition.py @@ -1,8 +1,8 @@ from .Key import Key -class Partition: - def __init__(self, name:str, notes:list[Key]) -> None: +class Partition: + def __init__(self, name: str, notes: list[Key]) -> None: self.__name = name self.notes = notes @@ -12,4 +12,3 @@ class Partition: for i in self.notes: r += f"{i.__repr__()}\n" return r - diff --git a/scorometer/main.py b/scorometer/main.py index 9db4cb0..de37b00 100755 --- a/scorometer/main.py +++ b/scorometer/main.py @@ -1,14 +1,22 @@ #!/usr/bin/python3 -from chroma_case.Partition import Partition -from chroma_case.Key import Key -import sys -import select -import os import itertools -import requests -import operator import json +import logging +import operator +import os +import select +import sys +from dataclasses import dataclass +from typing import Literal, Tuple + +import requests +from chroma_case.Key import Key +from chroma_case.Partition import Partition from mido import MidiFile +from validated_dc import ValidatedDC, get_errors, is_valid + +BACK_URL = os.environ.get("BACK_URL") or "http://back:3000" +MUSICS_FOLDER = os.environ.get("MUSICS_FOLDER") or "/musics/" RATIO = float(sys.argv[2] if len(sys.argv) > 2 else 1) OCTAVE = 5 @@ -19,33 +27,104 @@ NORMAL = 0 PRACTICE = 1 +@dataclass +class InvalidMessage: + message: str + + +@dataclass +class StartMessage(ValidatedDC): + id: int + bearer: str + mode: Literal["normal", "practice"] + type: Literal["start"] = "start" + + +@dataclass +class EndMessage(ValidatedDC): + type: Literal["end"] = "end" + + +@dataclass +class NoteOnMessage(ValidatedDC): + time: int + note: int + id: int + type: Literal["note_on"] = "note_on" + + +@dataclass +class NoteOffMessage(ValidatedDC): + time: int + note: int + id: int + type: Literal["note_off"] = "note_off" + + +@dataclass +class PauseMessage(ValidatedDC): + paused: bool + time: int + type: Literal["pause"] = "pause" + + +message_map = { + "start": StartMessage, + "end": EndMessage, + "note_on": NoteOnMessage, + "note_off": NoteOffMessage, + "pause": PauseMessage, +} + + +def getMessage() -> ( + Tuple[ + StartMessage + | EndMessage + | NoteOnMessage + | NoteOffMessage + | PauseMessage + | InvalidMessage, + str, + ] +): + try: + msg = input() + obj = json.loads(msg) + res = message_map[obj["type"]](**obj) + if is_valid(res): + return res, msg + else: + return InvalidMessage(str(get_errors(res))), msg + except Exception as e: + return InvalidMessage(str(e)), "" + + def send(o): print(json.dumps(o), flush=True) -def log(level, message): - send({"type": "log", "level": level, "message": message}) -def debug(message): - log("DEBUG", message) - -def info(message): - log("INFO", message) - -def warn(message): - log("WARN", message) - -def fatal(message): - log("FATAL", message) - -class Scorometer(): - def __init__(self, mode, midiFile) -> None: +class Scorometer: + def __init__(self, mode, midiFile, song_id, user_id) -> None: self.partition = self.getPartition(midiFile) self.keys_down = [] - self.score = 0 self.mode = mode + self.song_id = song_id + self.user_id = user_id + self.score = 0 + self.missed = 0 + self.perfect = 0 + self.great = 0 + self.good = 0 + self.difficulties = {} if mode == PRACTICE: get_start = operator.attrgetter("start") - self.practice_partition = [list(g) for _, g in itertools.groupby(sorted(self.partition.notes, key=get_start), get_start)] + self.practice_partition = [ + list(g) + for _, g in itertools.groupby( + sorted(self.partition.notes, key=get_start), get_start + ) + ] else: self.practice_partition: list[list[Key]] = [] @@ -56,165 +135,268 @@ class Scorometer(): prev_note_on = {} for msg in MidiFile(midiFile): d = msg.dict() - s += d['time'] * 1000 * RATIO + s += d["time"] * 1000 * RATIO if d["type"] == "note_on": prev_note_on[d["note"]] = 0 if d["note"] in notes_on: prev_note_on[d["note"]] = notes_on[d["note"]] # 500 - notes_on[d["note"]] = s # 0 + notes_on[d["note"]] = s # 0 if d["type"] == "note_off": duration = s - notes_on[d["note"]] note_start = notes_on[d["note"]] notes.append(Key(d["note"], note_start, duration - 10)) - notes_on[d["note"]] = s # 500 + notes_on[d["note"]] = s # 500 return Partition(midiFile, notes) - def handleNote(self, obj): - _key = obj["note"] - status = obj["type"] - timestamp = obj["time"] + def handleNoteOn(self, message: NoteOnMessage): + _key = message.note + timestamp = message.time is_down = any(x[0] == _key for x in self.keys_down) - key = None - if status == "note_on" and not is_down: + if not is_down: self.keys_down.append((_key, timestamp)) - debug({"note": _key}) - elif status == "note_off" or is_down: - down_since = next(since for (h_key, since) in self.keys_down if h_key == _key) - self.keys_down.remove((_key, down_since)) - key = Key(_key, down_since, (timestamp - down_since)) - #debug({key: key}) - if key is None: - return - to_play = next((i for i in self.partition.notes if i.key == key.key and self.is_timing_close(key, i) and i.done == False), None) - if to_play == None: + logging.debug({"note": _key}) + + def handleNoteOff(self, message: NoteOffMessage): + _key = message.note + timestamp = message.time + down_since = next(since for (h_key, since) in self.keys_down if h_key == _key) + self.keys_down.remove((_key, down_since)) + key = Key(_key, down_since, (timestamp - down_since)) + # debug({key: key}) + to_play = next( + ( + i + for i in self.partition.notes + if i.key == key.key and self.is_timing_close(key, i) and i.done is False + ), + None, + ) + if to_play is None: self.score -= 50 - debug(f"Invalid key.") + logging.info("Invalid key.") else: timingScore, timingInformation = self.getTiming(key, to_play) - self.score += 100 if timingScore == "perfect" else 75 if timingScore == "great" else 50 + self.score += ( + 100 + if timingScore == "perfect" + else 75 + if timingScore == "great" + else 50 + ) to_play.done = True - self.sendScore(obj["id"], timingScore, timingInformation) + self.sendScore(message.id, timingScore, timingInformation) - def handleNotePractice(self, obj): - _key = obj["note"] - status = obj["type"] - timestamp = obj["time"] + def handleNoteOnPractice(self, message: NoteOnMessage): + _key = message.note + timestamp = message.time is_down = any(x[0] == _key for x in self.keys_down) - key = None - if status == "note_on" and not is_down: + if not is_down: self.keys_down.append((_key, timestamp)) - debug({"note": _key}) - elif status == "note_off" or is_down: - down_since = next(since for (h_key, since) in self.keys_down if h_key == _key) - self.keys_down.remove((_key, down_since)) - key = Key(_key, down_since, (timestamp - down_since)) - #debug({key: key}) - if key is None: - return - keys_to_play = next((i for i in self.practice_partition if any(x.done != True for x in i)), None) + logging.debug({"note": _key}) + + def handleNoteOffPractice(self, message: NoteOffMessage): + _key = message.note + timestamp = message.time + # is_down = any(x[0] == _key for x in self.keys_down) + down_since = next(since for (h_key, since) in self.keys_down if h_key == _key) + self.keys_down.remove((_key, down_since)) + key = Key(_key, down_since, (timestamp - down_since)) + keys_to_play = next( + (i for i in self.practice_partition if any(x.done is not True for x in i)), + None, + ) if keys_to_play is None: - warn("Key sent but there is no keys to play") + logging.info("Key sent but there is no keys to play") self.score -= 50 return - to_play = next((i for i in keys_to_play if i.key == key.key and i.done != True), None) - if to_play == None: + to_play = next( + (i for i in keys_to_play if i.key == key.key and i.done is not True), None + ) + if to_play is None: self.score -= 50 - debug(f"Invalid key.") + logging.info("Invalid key.") else: timingScore, _ = self.getTiming(key, to_play) - self.score += 100 if timingScore == "perfect" else 75 if timingScore == "great" else 50 + self.score += ( + 100 + if timingScore == "perfect" + else 75 + if timingScore == "great" + else 50 + ) to_play.done = True - self.sendScore(obj["id"], timingScore, "practice") + self.sendScore(message.id, timingScore, "practice") def getTiming(self, key: Key, to_play: Key): return self.getTimingScore(key, to_play), self.getTimingInfo(key, to_play) def getTimingScore(self, key: Key, to_play: Key): tempo_percent = abs((key.duration / to_play.duration) - 1) - if tempo_percent < .3: + if tempo_percent < 0.3: timingScore = "perfect" - elif tempo_percent < .5: - timingScore = f"great" + elif tempo_percent < 0.5: + timingScore = "great" else: timingScore = "good" return timingScore def getTimingInfo(self, key: Key, to_play: Key): - return "perfect" if abs(key.start - to_play.start) < 200 else "fast" if key.start < to_play.start else "late" + return ( + "perfect" + if abs(key.start - to_play.start) < 200 + else "fast" + if key.start < to_play.start + else "late" + ) # is it in the 500 ms range def is_timing_close(self, key: Key, i: Key): return abs(i.start - key.start) < 500 - def handleMessage(self, message: str): - obj = json.loads(message) - if "type" not in obj.keys(): - warn(f"Could not handle message {message}") - return - if obj["type"] == "note_on" or obj["type"] == "note_off": - if self.mode == NORMAL: - self.handleNote(obj) - elif self.mode == PRACTICE: - self.handleNotePractice(obj) - if obj["type"] == "pause": - pass + def handleMessage( + self, + message: StartMessage + | EndMessage + | NoteOnMessage + | NoteOffMessage + | PauseMessage + | InvalidMessage, + line: str, + ): + match message: + case InvalidMessage(error): + logging.warning(f"Invalid message {line} with error: {error}") + send({"error": f"Invalid message {line} with error: {error}"}) + case NoteOnMessage(): + if self.mode == NORMAL: + self.handleNoteOn(message) + elif self.mode == PRACTICE: + self.handleNoteOnPractice(message) + case NoteOffMessage(): + if self.mode == NORMAL: + self.handleNoteOff(message) + elif self.mode == PRACTICE: + self.handleNoteOffPractice(message) + case PauseMessage(): + pass + case EndMessage(): + self.endGame() + case _: + logging.warning( + f"Expected note_on note_off or pause message but got {message.type} instead" + ) def sendScore(self, id, timingScore, timingInformation): - send({"id": id, "timingScore": timingScore, "timingInformation": timingInformation}) + send( + { + "id": id, + "timingScore": timingScore, + "timingInformation": timingInformation, + } + ) def gameLoop(self): while True: - if select.select([sys.stdin, ], [], [], 0.0)[0]: - line = input() - if not line: - break - info(f"handling message {line}") - self.handleMessage(line.rstrip()) + if select.select( + [ + sys.stdin, + ], + [], + [], + 0.0, + )[0]: + message, line = getMessage() + logging.info(f"handling message {line}") + self.handleMessage(message, line) else: pass - return self.score, {} -def handleStartMessage(start_message): - if "type" not in start_message.keys(): - raise Exception("type of start message not specified") - if start_message["type"] != "start": - raise Exception("start message is not of type start") - if "id" not in start_message.keys(): - raise Exception("id of song not specified in start message") - if "mode" not in start_message.keys(): - raise Exception("mode of song not specified in start message") - if "user_id" not in start_message.keys(): - raise Exception("user_id not specified in start message") - mode = PRACTICE if start_message["mode"] == "practice" else NORMAL - # TODO get song path from the API - song_id = start_message["id"] - # TODO: use something secure here but I don't find sending a jwt something elegant. - user_id = start_message["user_id"] - song_path = requests.get(f"http://back:3000/song/{song_id}").json()["midiPath"] + def endGame(self): + for i in self.partition.notes: + if i.done is False: + self.score -= 50 + send( + { + "overallScore": self.score, + "score": { + "missed": self.missed, + "good": self.good, + "great": self.great, + "perfect": self.perfect, + "maxScore": len(self.partition.notes) * 100, + }, + } + ) + if self.user_id != -1: + requests.post( + f"{BACK_URL}/history", + json={ + "songID": self.song_id, + "userID": self.user_id, + "score": self.score, + "difficulties": self.difficulties, + }, + ) + exit() + + +def handleStartMessage(start_message: StartMessage): + mode = PRACTICE if start_message.mode == "practice" else NORMAL + song_id = start_message.id + user_id = -1 + try: + if start_message.bearer != "": + r = requests.get( + f"{BACK_URL}/auth/me", + headers={"Authorization": f"Bearer {start_message.bearer}"}, + ) + r.raise_for_status() + user_id = r.json()["id"] + except Exception as e: + logging.fatal("Could not get user id with given bearer", exc_info=e) + send({"error": "Could not get user id with given bearer"}) + exit() + + try: + r = requests.get(f"{BACK_URL}/song/{song_id}") + r.raise_for_status() + song_path = r.json()["midiPath"] + song_path = song_path.replace("/musics/", MUSICS_FOLDER) + except Exception as e: + logging.fatal("Invalid song id", exc_info=e) + send({"error": "Invalid song id, song does not exist"}) + exit() return mode, song_path, song_id, user_id -def sendScore(score, difficulties, song_id, user_id): - send({"overallScore": score, "score": difficulties}) - requests.post(f"http://back:3000/history", json={ - "songID": song_id, - "userID": user_id, - "score": score, - "difficulties": difficulties, - }) +def startGame(start_message: StartMessage): + mode, song_path, song_id, user_id = handleStartMessage(start_message) + sc = Scorometer(mode, song_path, song_id, user_id) + sc.gameLoop() def main(): try: - start_message = json.loads(input()) - mode, song_path, song_id, user_id = handleStartMessage(start_message) - sc = Scorometer(mode, song_path) - score, difficulties = sc.gameLoop() - sendScore(score, difficulties, song_id, user_id) - except Exception as error: - send({ "error": error }) + msg, _ = getMessage() + match msg: + case StartMessage(): + startGame(msg) + case EndMessage(): + logging.info("scorometer ended before a start message") + send({"error": "Did not receive a start message"}) + exit() + case InvalidMessage(error): + logging.warning(f"invalid message with error: {error}") + send({"error": "Invalid input, expected a start message"}) + case _: + logging.warning(f"invalid message with type: {msg.type}") + send({"error": "Invalid input, expected a start message"}) + except Exception as e: + logging.fatal("error", exc_info=e) + send({"error": "a fatal error occured"}) + if __name__ == "__main__": main() diff --git a/scorometer/requirements.txt b/scorometer/requirements.txt index 4839c29..9e1e42e 100644 --- a/scorometer/requirements.txt +++ b/scorometer/requirements.txt @@ -1,2 +1,4 @@ mido requests +black-with-tabs +validated-dc diff --git a/scorometer/tests/almost_perfect_play/input b/scorometer/tests/almost_perfect_play/input new file mode 100644 index 0000000..12dfc5e --- /dev/null +++ b/scorometer/tests/almost_perfect_play/input @@ -0,0 +1,21 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 2, "time": 3750, "note": 67} +{"type": "note_off", "id": 2, "time": 3980, "note": 67} +{"type": "note_on", "id": 3, "time": 4000, "note": 62} +{"type": "note_off", "id": 3, "time": 4240, "note": 62} +{"type": "note_on", "id": 4, "time": 4000, "note": 64} +{"type": "note_off", "id": 4, "time": 4240, "note": 64} +{"type": "note_on", "id": 5, "time": 4000, "note": 60} +{"type": "note_off", "id": 5, "time": 4240, "note": 60} +{"type": "note_on", "id": 6, "time": 4500, "note": 63} +{"type": "note_off", "id": 6, "time": 4740, "note": 63} +{"type": "note_on", "id": 7, "time": 4750, "note": 63} +{"type": "note_off", "id": 7, "time": 4980, "note": 63} +{"type": "note_on", "id": 8, "time": 5000, "note": 63} +{"type": "note_off", "id": 8, "time": 5990, "note": 63} +{"type": "note_on", "id": 9, "time": 6500, "note": 62} +{"type": "note_off", "id": 9, "time": 6990, "note": 62} +{"type": "note_on", "id": 10, "time": 6750, "note": 60} +{"type": "note_off", "id": 10, "time": 7240, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/almost_perfect_play/output b/scorometer/tests/almost_perfect_play/output new file mode 100644 index 0000000..85463e6 --- /dev/null +++ b/scorometer/tests/almost_perfect_play/output @@ -0,0 +1,10 @@ +{"id": 2, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 7, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 8, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 9, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 10, "timingScore": "perfect", "timingInformation": "perfect"} +{"overallScore": 850, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/early/input b/scorometer/tests/early/input new file mode 100644 index 0000000..8279860 --- /dev/null +++ b/scorometer/tests/early/input @@ -0,0 +1,23 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3250, "note": 68} +{"type": "note_off", "id": 1, "time": 3490, "note": 68} +{"type": "note_on", "id": 2, "time": 3500, "note": 67} +{"type": "note_off", "id": 2, "time": 3730, "note": 67} +{"type": "note_on", "id": 3, "time": 3750, "note": 62} +{"type": "note_off", "id": 3, "time": 3990, "note": 62} +{"type": "note_on", "id": 4, "time": 3750, "note": 64} +{"type": "note_off", "id": 4, "time": 3990, "note": 64} +{"type": "note_on", "id": 5, "time": 3750, "note": 60} +{"type": "note_off", "id": 5, "time": 3990, "note": 60} +{"type": "note_on", "id": 6, "time": 4250, "note": 63} +{"type": "note_off", "id": 6, "time": 4490, "note": 63} +{"type": "note_on", "id": 7, "time": 4500, "note": 63} +{"type": "note_off", "id": 7, "time": 4730, "note": 63} +{"type": "note_on", "id": 8, "time": 4750, "note": 63} +{"type": "note_off", "id": 8, "time": 5740, "note": 63} +{"type": "note_on", "id": 9, "time": 6250, "note": 62} +{"type": "note_off", "id": 9, "time": 6740, "note": 62} +{"type": "note_on", "id": 10, "time": 6500, "note": 60} +{"type": "note_off", "id": 10, "time": 6990, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/early/output b/scorometer/tests/early/output new file mode 100644 index 0000000..77522b8 --- /dev/null +++ b/scorometer/tests/early/output @@ -0,0 +1,11 @@ +{"id": 1, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 2, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 3, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 4, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 5, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 6, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 7, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 8, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 9, "timingScore": "perfect", "timingInformation": "fast"} +{"id": 10, "timingScore": "perfect", "timingInformation": "fast"} +{"overallScore": 1000, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/end_miss/input b/scorometer/tests/end_miss/input new file mode 100644 index 0000000..907783b --- /dev/null +++ b/scorometer/tests/end_miss/input @@ -0,0 +1,19 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3500, "note": 68} +{"type": "note_off", "id": 1, "time": 3740, "note": 68} +{"type": "note_on", "id": 2, "time": 3750, "note": 67} +{"type": "note_off", "id": 2, "time": 3980, "note": 67} +{"type": "note_on", "id": 3, "time": 4000, "note": 62} +{"type": "note_off", "id": 3, "time": 4240, "note": 62} +{"type": "note_on", "id": 4, "time": 4000, "note": 64} +{"type": "note_off", "id": 4, "time": 4240, "note": 64} +{"type": "note_on", "id": 5, "time": 4000, "note": 60} +{"type": "note_off", "id": 5, "time": 4240, "note": 60} +{"type": "note_on", "id": 6, "time": 4500, "note": 63} +{"type": "note_off", "id": 6, "time": 4740, "note": 63} +{"type": "note_on", "id": 7, "time": 4750, "note": 63} +{"type": "note_off", "id": 7, "time": 4980, "note": 63} +{"type": "note_on", "id": 8, "time": 5000, "note": 63} +{"type": "note_off", "id": 8, "time": 5990, "note": 63} +{"type": "end"} + diff --git a/scorometer/tests/end_miss/output b/scorometer/tests/end_miss/output new file mode 100644 index 0000000..086b2a9 --- /dev/null +++ b/scorometer/tests/end_miss/output @@ -0,0 +1,9 @@ +{"id": 1, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 2, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 7, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 8, "timingScore": "perfect", "timingInformation": "perfect"} +{"overallScore": 700, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/hold_not_enough/input b/scorometer/tests/hold_not_enough/input new file mode 100644 index 0000000..401d679 --- /dev/null +++ b/scorometer/tests/hold_not_enough/input @@ -0,0 +1,23 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3500, "note": 68} +{"type": "note_off", "id": 1, "time": 3540, "note": 68} +{"type": "note_on", "id": 2, "time": 3750, "note": 67} +{"type": "note_off", "id": 2, "time": 3780, "note": 67} +{"type": "note_on", "id": 3, "time": 4000, "note": 62} +{"type": "note_off", "id": 3, "time": 4040, "note": 62} +{"type": "note_on", "id": 4, "time": 4000, "note": 64} +{"type": "note_off", "id": 4, "time": 4040, "note": 64} +{"type": "note_on", "id": 5, "time": 4000, "note": 60} +{"type": "note_off", "id": 5, "time": 4040, "note": 60} +{"type": "note_on", "id": 6, "time": 4500, "note": 63} +{"type": "note_off", "id": 6, "time": 4540, "note": 63} +{"type": "note_on", "id": 7, "time": 4750, "note": 63} +{"type": "note_off", "id": 7, "time": 4780, "note": 63} +{"type": "note_on", "id": 8, "time": 5000, "note": 63} +{"type": "note_off", "id": 8, "time": 5290, "note": 63} +{"type": "note_on", "id": 9, "time": 6500, "note": 62} +{"type": "note_off", "id": 9, "time": 6690, "note": 62} +{"type": "note_on", "id": 10, "time": 6750, "note": 60} +{"type": "note_off", "id": 10, "time": 6840, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/hold_not_enough/output b/scorometer/tests/hold_not_enough/output new file mode 100644 index 0000000..6307a91 --- /dev/null +++ b/scorometer/tests/hold_not_enough/output @@ -0,0 +1,11 @@ +{"id": 1, "timingScore": "good", "timingInformation": "perfect"} +{"id": 2, "timingScore": "good", "timingInformation": "perfect"} +{"id": 3, "timingScore": "good", "timingInformation": "perfect"} +{"id": 4, "timingScore": "good", "timingInformation": "perfect"} +{"id": 5, "timingScore": "good", "timingInformation": "perfect"} +{"id": 6, "timingScore": "good", "timingInformation": "perfect"} +{"id": 7, "timingScore": "good", "timingInformation": "perfect"} +{"id": 8, "timingScore": "good", "timingInformation": "perfect"} +{"id": 9, "timingScore": "good", "timingInformation": "perfect"} +{"id": 10, "timingScore": "good", "timingInformation": "perfect"} +{"overallScore": 500, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/hold_too_long/input b/scorometer/tests/hold_too_long/input new file mode 100644 index 0000000..e9f1585 --- /dev/null +++ b/scorometer/tests/hold_too_long/input @@ -0,0 +1,23 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3500, "note": 68} +{"type": "note_off", "id": 1, "time": 3990, "note": 68} +{"type": "note_on", "id": 2, "time": 3750, "note": 67} +{"type": "note_off", "id": 2, "time": 4230, "note": 67} +{"type": "note_on", "id": 3, "time": 4000, "note": 62} +{"type": "note_off", "id": 3, "time": 4490, "note": 62} +{"type": "note_on", "id": 4, "time": 4000, "note": 64} +{"type": "note_off", "id": 4, "time": 4490, "note": 64} +{"type": "note_on", "id": 5, "time": 4000, "note": 60} +{"type": "note_off", "id": 5, "time": 4490, "note": 60} +{"type": "note_on", "id": 6, "time": 4500, "note": 63} +{"type": "note_off", "id": 6, "time": 4990, "note": 63} +{"type": "note_on", "id": 7, "time": 4750, "note": 63} +{"type": "note_off", "id": 7, "time": 5230, "note": 63} +{"type": "note_on", "id": 8, "time": 5000, "note": 63} +{"type": "note_off", "id": 8, "time": 6490, "note": 63} +{"type": "note_on", "id": 9, "time": 6500, "note": 62} +{"type": "note_off", "id": 9, "time": 7240, "note": 62} +{"type": "note_on", "id": 10, "time": 6750, "note": 60} +{"type": "note_off", "id": 10, "time": 7490, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/hold_too_long/output b/scorometer/tests/hold_too_long/output new file mode 100644 index 0000000..6307a91 --- /dev/null +++ b/scorometer/tests/hold_too_long/output @@ -0,0 +1,11 @@ +{"id": 1, "timingScore": "good", "timingInformation": "perfect"} +{"id": 2, "timingScore": "good", "timingInformation": "perfect"} +{"id": 3, "timingScore": "good", "timingInformation": "perfect"} +{"id": 4, "timingScore": "good", "timingInformation": "perfect"} +{"id": 5, "timingScore": "good", "timingInformation": "perfect"} +{"id": 6, "timingScore": "good", "timingInformation": "perfect"} +{"id": 7, "timingScore": "good", "timingInformation": "perfect"} +{"id": 8, "timingScore": "good", "timingInformation": "perfect"} +{"id": 9, "timingScore": "good", "timingInformation": "perfect"} +{"id": 10, "timingScore": "good", "timingInformation": "perfect"} +{"overallScore": 500, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/invalid_song/input b/scorometer/tests/invalid_song/input new file mode 100644 index 0000000..5e25a20 --- /dev/null +++ b/scorometer/tests/invalid_song/input @@ -0,0 +1,2 @@ +{"type":"start", "id": 0, "mode": "normal", "bearer": ""} + diff --git a/scorometer/tests/invalid_song/output b/scorometer/tests/invalid_song/output new file mode 100644 index 0000000..9da53e1 --- /dev/null +++ b/scorometer/tests/invalid_song/output @@ -0,0 +1 @@ +{"error": "Invalid song id, song does not exist"} diff --git a/scorometer/tests/late/input b/scorometer/tests/late/input new file mode 100644 index 0000000..d100c13 --- /dev/null +++ b/scorometer/tests/late/input @@ -0,0 +1,23 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3750, "note": 68} +{"type": "note_off", "id": 1, "time": 3990, "note": 68} +{"type": "note_on", "id": 2, "time": 4000, "note": 67} +{"type": "note_off", "id": 2, "time": 4230, "note": 67} +{"type": "note_on", "id": 3, "time": 4250, "note": 62} +{"type": "note_off", "id": 3, "time": 4490, "note": 62} +{"type": "note_on", "id": 4, "time": 4250, "note": 64} +{"type": "note_off", "id": 4, "time": 4490, "note": 64} +{"type": "note_on", "id": 5, "time": 4250, "note": 60} +{"type": "note_off", "id": 5, "time": 4490, "note": 60} +{"type": "note_on", "id": 6, "time": 4750, "note": 63} +{"type": "note_off", "id": 6, "time": 4990, "note": 63} +{"type": "note_on", "id": 7, "time": 5000, "note": 63} +{"type": "note_off", "id": 7, "time": 5230, "note": 63} +{"type": "note_on", "id": 8, "time": 5250, "note": 63} +{"type": "note_off", "id": 8, "time": 6240, "note": 63} +{"type": "note_on", "id": 9, "time": 6750, "note": 62} +{"type": "note_off", "id": 9, "time": 7240, "note": 62} +{"type": "note_on", "id": 10, "time": 7000, "note": 60} +{"type": "note_off", "id": 10, "time": 7490, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/late/output b/scorometer/tests/late/output new file mode 100644 index 0000000..28c6d1f --- /dev/null +++ b/scorometer/tests/late/output @@ -0,0 +1,11 @@ +{"id": 1, "timingScore": "perfect", "timingInformation": "late"} +{"id": 2, "timingScore": "perfect", "timingInformation": "late"} +{"id": 3, "timingScore": "perfect", "timingInformation": "late"} +{"id": 4, "timingScore": "perfect", "timingInformation": "late"} +{"id": 5, "timingScore": "perfect", "timingInformation": "late"} +{"id": 6, "timingScore": "perfect", "timingInformation": "late"} +{"id": 7, "timingScore": "perfect", "timingInformation": "late"} +{"id": 8, "timingScore": "perfect", "timingInformation": "late"} +{"id": 9, "timingScore": "perfect", "timingInformation": "late"} +{"id": 10, "timingScore": "perfect", "timingInformation": "late"} +{"overallScore": 1000, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/perfect_play/input b/scorometer/tests/perfect_play/input new file mode 100644 index 0000000..ce6bc30 --- /dev/null +++ b/scorometer/tests/perfect_play/input @@ -0,0 +1,23 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3500, "note": 68} +{"type": "note_off", "id": 1, "time": 3740, "note": 68} +{"type": "note_on", "id": 2, "time": 3750, "note": 67} +{"type": "note_off", "id": 2, "time": 3980, "note": 67} +{"type": "note_on", "id": 3, "time": 4000, "note": 62} +{"type": "note_off", "id": 3, "time": 4240, "note": 62} +{"type": "note_on", "id": 4, "time": 4000, "note": 64} +{"type": "note_off", "id": 4, "time": 4240, "note": 64} +{"type": "note_on", "id": 5, "time": 4000, "note": 60} +{"type": "note_off", "id": 5, "time": 4240, "note": 60} +{"type": "note_on", "id": 6, "time": 4500, "note": 63} +{"type": "note_off", "id": 6, "time": 4740, "note": 63} +{"type": "note_on", "id": 7, "time": 4750, "note": 63} +{"type": "note_off", "id": 7, "time": 4980, "note": 63} +{"type": "note_on", "id": 8, "time": 5000, "note": 63} +{"type": "note_off", "id": 8, "time": 5990, "note": 63} +{"type": "note_on", "id": 9, "time": 6500, "note": 62} +{"type": "note_off", "id": 9, "time": 6990, "note": 62} +{"type": "note_on", "id": 10, "time": 6750, "note": 60} +{"type": "note_off", "id": 10, "time": 7240, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/perfect_play/output b/scorometer/tests/perfect_play/output new file mode 100644 index 0000000..f35d8ce --- /dev/null +++ b/scorometer/tests/perfect_play/output @@ -0,0 +1,11 @@ +{"id": 1, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 2, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 7, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 8, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 9, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 10, "timingScore": "perfect", "timingInformation": "perfect"} +{"overallScore": 1000, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/random_miss/input b/scorometer/tests/random_miss/input new file mode 100644 index 0000000..a5b0e5b --- /dev/null +++ b/scorometer/tests/random_miss/input @@ -0,0 +1,19 @@ +{"type":"start", "id": 1, "mode": "normal", "bearer": ""} +{"type": "note_on", "id": 1, "time": 3500, "note": 68} +{"type": "note_off", "id": 1, "time": 3740, "note": 68} +{"type": "note_on", "id": 3, "time": 4000, "note": 62} +{"type": "note_off", "id": 3, "time": 4240, "note": 62} +{"type": "note_on", "id": 4, "time": 4000, "note": 64} +{"type": "note_off", "id": 4, "time": 4240, "note": 64} +{"type": "note_on", "id": 5, "time": 4000, "note": 60} +{"type": "note_off", "id": 5, "time": 4240, "note": 60} +{"type": "note_on", "id": 6, "time": 4500, "note": 63} +{"type": "note_off", "id": 6, "time": 4740, "note": 63} +{"type": "note_on", "id": 8, "time": 5000, "note": 63} +{"type": "note_off", "id": 8, "time": 5990, "note": 63} +{"type": "note_on", "id": 9, "time": 6500, "note": 62} +{"type": "note_off", "id": 9, "time": 6990, "note": 62} +{"type": "note_on", "id": 10, "time": 6750, "note": 60} +{"type": "note_off", "id": 10, "time": 7240, "note": 60} +{"type": "end"} + diff --git a/scorometer/tests/random_miss/output b/scorometer/tests/random_miss/output new file mode 100644 index 0000000..40b5a92 --- /dev/null +++ b/scorometer/tests/random_miss/output @@ -0,0 +1,9 @@ +{"id": 1, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 8, "timingScore": "good", "timingInformation": "late"} +{"id": 9, "timingScore": "perfect", "timingInformation": "perfect"} +{"id": 10, "timingScore": "perfect", "timingInformation": "perfect"} +{"overallScore": 650, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}} diff --git a/scorometer/tests/runner.sh b/scorometer/tests/runner.sh new file mode 100755 index 0000000..7844adb --- /dev/null +++ b/scorometer/tests/runner.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +EMPTY_DB=$(curl localhost:3000/song/1 -s | jq '.statusCode == 404') +if [[ $EMPTY_DB == "true" ]]; then + curl localhost:3000/song -X POST --data '{"name": "SCORO_TEST", "difficulties": {}, "midiPath": "/musics/SCORO_TEST/SCORO_TEST.midi", "musicXmlPath": "/musics/SCORO_TEST/SCORO_TEST.mxl"}' -H "Content-Type: application/json" &> /dev/null +fi + +TESTS_DONE=0 +TESTS_SUCCESS=0 +TESTS_FAILED=0 + +function test { + cat $1/input | BACK_URL="http://localhost:3000" MUSICS_FOLDER="../../musics/" python3 ../main.py 1> /tmp/scorometer_res 2> /tmp/scorometer_log + TESTS_DONE=$((TESTS_DONE + 1)) + if ! diff $1/output /tmp/scorometer_res &>/dev/null; then + echo "$t failed, do runner.sh $t for more info" + TESTS_FAILED=$((TESTS_FAILED + 1)) + else + TESTS_SUCCESS=$((TESTS_SUCCESS + 1)) + fi +} + +if [ -z "$1" ]; +then + for t in */; do + test $t + done + exit $TESTS_FAILED +else + cat $1/input | BACK_URL="http://localhost:3000" MUSICS_FOLDER="../../musics/" python3 ../main.py 1> /tmp/scorometer_res 2> /tmp/scorometer_log + echo "=========== CURRENT OUTPUT ===========" + cat /tmp/scorometer_res + echo "======================================" + echo "=========== EXPECTED OUTPUT ==========" + cat $1/output + echo "======================================" + echo "=============== DIFF =================" + diff --side-by-side -q /tmp/scorometer_res $1/output + RET=$? + echo "======================================" + exit $RET +fi; + diff --git a/scorometer/tests/test_midi.png b/scorometer/tests/test_midi.png new file mode 100644 index 0000000..84f68ca Binary files /dev/null and b/scorometer/tests/test_midi.png differ