Merge branch 'main' of github.com:Chroma-Case/Chromacase into front/play-page-connection

This commit is contained in:
Arthur Jamet
2023-04-02 15:44:23 +01:00
30 changed files with 656 additions and 146 deletions

View File

@@ -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:

View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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})"

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -1,2 +1,4 @@
mido
requests
black-with-tabs
validated-dc

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -0,0 +1,2 @@
{"type":"start", "id": 0, "mode": "normal", "bearer": ""}

View File

@@ -0,0 +1 @@
{"error": "Invalid song id, song does not exist"}

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -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"}

View File

@@ -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}}

View File

@@ -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"}

View File

@@ -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}}

43
scorometer/tests/runner.sh Executable file
View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB