From f26a6e8f9019d2e1517fa854652248f34059a00a Mon Sep 17 00:00:00 2001 From: Mack Wang Date: Tue, 7 Apr 2026 16:14:07 -0700 Subject: [PATCH] feat: add whitelist character support in repo toml This commit introduces a new key, `health-check.whitelisted-chars` for repo.toml. It allows TAs to configure repo-wide allowed non ASCII chars for the repo-health-checker binary. It results in a new command line switch, `-whitelisted-chars=X,Y,Z`, in the generated task.json. --- joj3_config_generator/models/repo.py | 17 ++ joj3_config_generator/transformers/repo.py | 15 +- tests/convert/test_convert_cases.py | 4 + tests/convert/whitelisted-chars/repo.toml | 1 + tests/convert/whitelisted-chars/task.json | 188 +++++++++++++++++++++ tests/convert/whitelisted-chars/task.toml | 2 + 6 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 tests/convert/whitelisted-chars/repo.toml create mode 100644 tests/convert/whitelisted-chars/task.json create mode 100644 tests/convert/whitelisted-chars/task.toml diff --git a/joj3_config_generator/models/repo.py b/joj3_config_generator/models/repo.py index d97da28..87734a9 100644 --- a/joj3_config_generator/models/repo.py +++ b/joj3_config_generator/models/repo.py @@ -50,6 +50,9 @@ class HealthCheck(StrictBaseModel): required_files: List[str] = Field( [], validation_alias=AliasChoices("required-files", "required_files") ) + whitelisted_chars: List[str] = Field( + [], validation_alias=AliasChoices("whitelisted-chars", "whitelisted_chars") + ) @field_validator("max_size", mode="before") @classmethod @@ -58,6 +61,20 @@ class HealthCheck(StrictBaseModel): return Memory(v) raise ValueError(f'Must be a string, e.g., "256m" or "1g", but got {v}') + @field_validator("whitelisted_chars") + @classmethod + def ensure_non_ascii_single_char_list(cls, chars: List[str]) -> List[str]: + for c in chars: + if len(c) != 1: + raise ValueError( + "Each whitelisted character must be exactly one character" + ) + if c.isascii(): + raise ValueError( + "Each whitelisted character must be a non-ASCII character" + ) + return chars + class Config(StrictBaseModel): root: Path = Field(Path("."), exclude=True) diff --git a/joj3_config_generator/transformers/repo.py b/joj3_config_generator/transformers/repo.py index 7e50439..5eced13 100644 --- a/joj3_config_generator/transformers/repo.py +++ b/joj3_config_generator/transformers/repo.py @@ -123,14 +123,23 @@ def get_check_lists(repo_conf: repo.Config) -> Tuple[List[str], List[str]]: def get_health_check_args(repo_conf: repo.Config) -> List[str]: file_sums, file_paths = get_check_lists(repo_conf) - return [ + args = [ "/usr/local/bin/repo-health-checker", "-root=.", f"-repoSize={str(repo_conf.health_check.max_size / 1024 / 1024)}", # B -> MB *[f"-meta={meta}" for meta in repo_conf.health_check.required_files], - f"-checkFileSumList={','.join(file_sums)}", - f"-checkFileNameList={','.join(file_paths)}", ] + if repo_conf.health_check.whitelisted_chars: + args.append( + f"-whitelisted-chars={','.join(repo_conf.health_check.whitelisted_chars)}" + ) + args.extend( + [ + f"-checkFileSumList={','.join(file_sums)}", + f"-checkFileNameList={','.join(file_paths)}", + ] + ) + return args def get_teapot_check_args(repo_conf: repo.Config, task_conf: task.Config) -> List[str]: diff --git a/tests/convert/test_convert_cases.py b/tests/convert/test_convert_cases.py index 284043a..2beea68 100644 --- a/tests/convert/test_convert_cases.py +++ b/tests/convert/test_convert_cases.py @@ -51,3 +51,7 @@ def test_result_detail() -> None: def test_unnecessary() -> None: load_case("unnecessary") + + +def test_whitelisted_chars() -> None: + load_case("whitelisted-chars") diff --git a/tests/convert/whitelisted-chars/repo.toml b/tests/convert/whitelisted-chars/repo.toml new file mode 100644 index 0000000..b62e42b --- /dev/null +++ b/tests/convert/whitelisted-chars/repo.toml @@ -0,0 +1 @@ +health-check.whitelisted-chars = ["你", "好", "!"] diff --git a/tests/convert/whitelisted-chars/task.json b/tests/convert/whitelisted-chars/task.json new file mode 100644 index 0000000..21b4e90 --- /dev/null +++ b/tests/convert/whitelisted-chars/task.json @@ -0,0 +1,188 @@ +{ + "name": "health check", + "logPath": "/home/tt/.cache/joj3/health/joj3.log", + "actorCsvPath": "/home/tt/.config/joj/students.csv", + "sandboxExecServer": "172.17.0.1:5051", + "sandboxToken": "", + "outputPath": "/tmp/joj3_result.json", + "preStages": [], + "stages": [ + { + "name": "Health Check", + "groups": [], + "executor": { + "name": "local", + "with": { + "default": { + "args": [], + "env": [ + "PATH=/usr/bin:/bin:/usr/local/bin" + ], + "stdin": { + "content": "" + }, + "stdout": { + "name": "stdout", + "max": 33554432, + "pipe": true + }, + "stderr": { + "name": "stderr", + "max": 33554432, + "pipe": true + }, + "cpuLimit": 10000000000, + "clockLimit": 20000000000, + "memoryLimit": 268435456, + "stackLimit": 0, + "procLimit": 50, + "cpuRateLimit": 0, + "cpuSetLimit": "", + "copyIn": {}, + "copyInCached": {}, + "copyInDir": ".", + "copyOut": [ + "stdout", + "stderr" + ], + "copyOutCached": [], + "copyOutMax": 0, + "copyOutDir": "", + "tty": false, + "strictMemoryLimit": false, + "dataSegmentLimit": false, + "addressSpaceLimit": false + }, + "cases": [ + { + "args": [ + "/usr/local/bin/repo-health-checker", + "-root=.", + "-repoSize=10.0", + "-whitelisted-chars=你,好,!", + "-checkFileSumList=", + "-checkFileNameList=" + ] + }, + { + "args": [ + "/usr/local/bin/joint-teapot", + "joj3-check-env", + "/home/tt/.config/teapot/teapot.env", + "--grading-repo-name", + "joj3-config-generator", + "--scoreboard-filename", + "scoreboard.csv" + ], + "env": [ + "REPOS_DIR=/home/tt/.cache", + "LOG_FILE_PATH=/home/tt/.cache/joint-teapot-debug.log" + ] + } + ] + } + }, + "parsers": [ + { + "name": "healthcheck", + "with": { + "score": 0 + } + }, + { + "name": "debug", + "with": {} + } + ] + } + ], + "postStages": [ + { + "name": "teapot", + "groups": [], + "executor": { + "name": "local", + "with": { + "default": { + "args": [ + "/usr/local/bin/joint-teapot", + "joj3-all-env", + "/home/tt/.config/teapot/teapot.env", + "--grading-repo-name", + "joj3-config-generator", + "--max-total-score", + "0", + "--issue-label-name", + "Kind/Testing", + "--issue-label-color", + "#795548", + "--scoreboard-filename", + "scoreboard.csv" + ], + "env": [ + "REPOS_DIR=/home/tt/.cache", + "LOG_FILE_PATH=/home/tt/.cache/joint-teapot-debug.log" + ], + "stdin": { + "content": "" + }, + "stdout": { + "name": "stdout", + "max": 33554432, + "pipe": true + }, + "stderr": { + "name": "stderr", + "max": 33554432, + "pipe": true + }, + "cpuLimit": 30000000000, + "clockLimit": 60000000000, + "memoryLimit": 268435456, + "stackLimit": 0, + "procLimit": 50, + "cpuRateLimit": 0, + "cpuSetLimit": "", + "copyIn": {}, + "copyInCached": {}, + "copyInDir": ".", + "copyOut": [ + "stdout", + "stderr" + ], + "copyOutCached": [], + "copyOutMax": 0, + "copyOutDir": "", + "tty": false, + "strictMemoryLimit": false, + "dataSegmentLimit": false, + "addressSpaceLimit": false + }, + "cases": [] + } + }, + "parsers": [ + { + "name": "log", + "with": { + "filename": "stdout", + "msg": "joj3 summary", + "level": -4 + } + }, + { + "name": "log", + "with": { + "filename": "stderr", + "msg": "joint-teapot stderr", + "level": 0 + } + }, + { + "name": "debug", + "with": {} + } + ] + } + ] +} diff --git a/tests/convert/whitelisted-chars/task.toml b/tests/convert/whitelisted-chars/task.toml new file mode 100644 index 0000000..e42a6ce --- /dev/null +++ b/tests/convert/whitelisted-chars/task.toml @@ -0,0 +1,2 @@ +name = "health check" +max-total-score = 0