Skip to content

Commit 54ac128

Browse files
authored
Adding unit tests (#319)
1 parent 3a55db3 commit 54ac128

File tree

8 files changed

+1118
-24
lines changed

8 files changed

+1118
-24
lines changed

.github/workflows/runner_ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,11 @@ jobs:
8383
env:
8484
CUDA_VISIBLE_DEVICES: 0
8585

86+
unit-tests:
87+
runs-on: ubuntu-latest
88+
timeout-minutes: 10
89+
steps:
90+
- uses: actions/checkout@v4
91+
- uses: astral-sh/setup-uv@v4
92+
- run: uv sync --extra dev
93+
- run: uv run pytest unit-tests -v

src/libkernelbot/submission.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def prepare_submission(
4747
)
4848

4949
if profanity.contains_profanity(req.file_name):
50-
raise KernelBotError("Please provide a non rude filename")
50+
raise KernelBotError("Please provide a non-rude filename")
5151

5252
# check file extension
5353
if not req.file_name.endswith((".py", ".cu", ".cuh", ".cpp")):
@@ -137,7 +137,7 @@ def handle_popcorn_directives(req: SubmissionRequest) -> SubmissionRequest:
137137
return req
138138

139139

140-
def _get_popcorn_directives(submission: str) -> dict:
140+
def _get_popcorn_directives(submission: str) -> dict: # noqa: C901
141141
popcorn_info = {"gpus": None, "leaderboard": None}
142142
for line in submission.splitlines():
143143
# only process the first comment block of the file.
@@ -148,10 +148,26 @@ def _get_popcorn_directives(submission: str) -> dict:
148148
args = line.split()
149149
if args[0] in ["//!POPCORN", "#!POPCORN"]:
150150
arg = args[1].strip().lower()
151-
if arg in ["gpu", "gpus"]:
151+
if len(args) < 3:
152+
raise KernelBotError(f"!POPCORN directive missing argument: {line}")
153+
# allow both versions of the argument
154+
if arg == "gpu":
155+
arg = "gpus"
156+
157+
if arg not in popcorn_info:
158+
raise KernelBotError(f"Invalid !POPCORN directive: {arg}")
159+
160+
if popcorn_info[arg] is not None:
161+
raise KernelBotError(f"Found multiple values for !POPCORN directive {arg}")
162+
163+
if arg == "gpus":
152164
popcorn_info["gpus"] = args[2:]
153165
elif arg == "leaderboard":
154-
popcorn_info["leaderboard"] = args[2]
166+
popcorn_info["leaderboard"] = args[2].strip()
167+
if len(popcorn_info["leaderboard"]) == 0:
168+
raise KernelBotError(
169+
"No leaderboard specified in !POPCORN Leaderboard directive"
170+
)
155171
return popcorn_info
156172

157173

src/libkernelbot/task.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ class LeaderboardTask:
6262
ranking_by: RankCriterion = RankCriterion.LAST
6363
seed: Optional[int] = None
6464

65+
def __post_init__(self):
66+
if self.lang == Language.Python and not isinstance(self.config, PythonTaskData):
67+
raise TypeError("Python language requires PythonTaskData config")
68+
if self.lang == Language.CUDA and not isinstance(self.config, CudaTaskData):
69+
raise TypeError("CUDA language requires CudaTaskData config")
70+
6571
@classmethod
6672
def from_dict(cls, data: dict):
6773
data_ = copy.copy(data)
@@ -147,7 +153,7 @@ def make_task_definition(yaml_file: str | Path) -> LeaderboardDefinition:
147153

148154

149155
def build_task_config(
150-
task: "LeaderboardTask" = None,
156+
task: LeaderboardTask = None,
151157
submission_content: str = None,
152158
arch: str = None,
153159
mode: SubmissionMode = None,

src/libkernelbot/utils.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ def __init__(self, max_size: int):
6060
self._max_size = max_size
6161
self._q = []
6262

63-
def __getitem__(self, key: Any, default: Any = None) -> Any | None:
63+
def __getitem__(self, key: Any) -> Any | None:
6464
if key not in self._cache:
65-
return default
65+
return None
6666

6767
self._q.remove(key)
6868
self._q.append(key)
@@ -95,50 +95,51 @@ def invalidate(self):
9595
self._q.clear()
9696

9797

98-
def format_time(value: float | str, err: Optional[float | str] = None, scale=None): # noqa: C901
99-
if value is None:
98+
def format_time(nanoseconds: float | str, err: Optional[float | str] = None): # noqa: C901
99+
if nanoseconds is None:
100100
logging.warning("Expected a number, got None", stack_info=True)
101101
return "–"
102102

103103
# really ugly, but works for now
104-
value = float(value)
104+
nanoseconds = float(nanoseconds)
105105

106106
scale = 1 # nanoseconds
107107
unit = "ns"
108-
if value > 2_000_000:
108+
if nanoseconds > 2_000_000:
109109
scale = 1000_000
110110
unit = "ms"
111-
elif value > 2000:
111+
elif nanoseconds > 2000:
112112
scale = 1000
113113
unit = "µs"
114114

115-
value /= scale
115+
time_in_unit = nanoseconds / scale
116116
if err is not None:
117117
err = float(err)
118118
err /= scale
119-
if value < 1:
119+
if time_in_unit < 1:
120120
if err:
121-
return f"{value} ± {err} {unit}"
121+
return f"{time_in_unit} ± {err} {unit}"
122122
else:
123-
return f"{value} {unit}"
124-
elif value < 10:
123+
return f"{time_in_unit} {unit}"
124+
elif time_in_unit < 10:
125125
if err:
126-
return f"{value:.2f} ± {err:.3f} {unit}"
126+
return f"{time_in_unit:.2f} ± {err:.3f} {unit}"
127127
else:
128-
return f"{value:.2f} {unit}"
129-
elif value < 100:
128+
return f"{time_in_unit:.2f} {unit}"
129+
elif time_in_unit < 100:
130130
if err:
131-
return f"{value:.1f} ± {err:.2f} {unit}"
131+
return f"{time_in_unit:.1f} ± {err:.2f} {unit}"
132132
else:
133-
return f"{value:.1f} {unit}"
133+
return f"{time_in_unit:.1f} {unit}"
134134
else:
135135
if err:
136-
return f"{value:.0f} ± {err:.1f} {unit}"
136+
return f"{time_in_unit:.0f} ± {err:.1f} {unit}"
137137
else:
138-
return f"{value:.0f} {unit}"
138+
return f"{time_in_unit:.0f} {unit}"
139139

140140

141141
def limit_length(text: str, maxlen: int):
142+
assert maxlen > 6
142143
if len(text) > maxlen:
143144
return text[: maxlen - 6] + " [...]"
144145
else:

0 commit comments

Comments
 (0)