Compare commits

...

87 Commits

Author SHA1 Message Date
8176ea8c5e feat: add whitelist character support in repo toml (#29)
All checks were successful
build / build (push) Successful in 8m39s
build / trigger-build-image (push) Successful in 1m36s
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.

Co-Authored-By: GitHub Copilot <noreply@microsoft.com>
<details>
<summary>Copilot Prompt</summary>
<br>
This repository contains a Python app that does conversion from TOML config files to a complex, multistage JSON config file for an online judge system. For example, under `tests/convert/full`, input are the two TOML files `repo.toml` and `task.toml`, output is one JSON file `task,json`.

Now, I want the repo-specific config (**repo.toml**) to accept an extra dotted key, "health-check.whitelisted-chars". This key shall accept an array of UTF-8 non-ASCII characters. To do so, I want you to
- Model. Under `joj3_config_generator/models/repo.py`, add an extra field *whitelisted_chars* to class *HealthCheck*, identified by both "whitelisted-chars" and "whitelisted_chars";
- Transforming. Under `joj3_config_generator/transforers/repo.py`, translate the field to an additional command line switch `-whitelisted-chars`, comma-separated.
- Other files you deem necessary, based on your understanding of this repo.

IMPORTANT. Before you start, explore this repo to under the file structure and file-function relations.

This repo uses the PDM package manager. After you finish your work, test your work appropriately. You should create new testcases under `tests/`.
- Details: Read the output `task.json` after running the test, to verify whether the command line switch was added to the *Health Check* `stage` or not.
</details>

Reviewed-on: https://focs.ji.sjtu.edu.cn/git/JOJ/JOJ3-config-generator/pulls/29
Reviewed-by: 李衍志523370910113 <jon-lee@sjtu.edu.cn>
Reviewed-by: 张泊明518370910136 <bomingzh@sjtu.edu.cn>
Co-authored-by: Mack Wang <mac-wang@outlook.com>
Co-committed-by: Mack Wang <mac-wang@outlook.com>
2026-04-22 17:06:54 +08:00
3c0195e605
feat: generate failure json for wrong toml files
All checks were successful
build / build (push) Successful in 2m41s
build / trigger-build-image (push) Successful in 10s
2025-11-27 21:15:45 -08:00
6c3fb51385
chore: update test results
All checks were successful
build / build (push) Successful in 2m42s
build / trigger-build-image (push) Successful in 11s
2025-11-27 20:49:14 -08:00
2d3490832a
feat: info log joint-teapot joj3-all-env stderr
Some checks failed
build / build (push) Failing after 3m17s
build / trigger-build-image (push) Has been skipped
2025-11-27 20:43:54 -08:00
2f5288e4c3
fix: sort by stdout
All checks were successful
build / build (push) Successful in 1m52s
build / trigger-build-image (push) Successful in 9s
2025-11-21 21:28:18 -08:00
4d7c7aa344
feat: natural sort cases
All checks were successful
build / build (push) Successful in 2m39s
build / trigger-build-image (push) Successful in 19s
2025-11-21 20:12:27 -08:00
4b2e818538
feat: support groups.ignore-submitter
All checks were successful
build / build (push) Successful in 2m20s
build / trigger-build-image (push) Successful in 13s
2025-10-30 19:52:08 -07:00
f570ea5aca
feat: support skip scoreboard and failed table
All checks were successful
build / build (push) Successful in 2m3s
build / trigger-build-image (push) Successful in 14s
2025-10-26 20:00:05 -07:00
6de91e0559
chore: update test results
All checks were successful
build / build (push) Successful in 2m1s
build / trigger-build-image (push) Successful in 11s
2025-10-13 02:50:18 -07:00
98d05a60b2
feat: remove unused field
Some checks failed
build / build (push) Failing after 1m39s
build / trigger-build-image (push) Has been skipped
2025-10-13 02:42:21 -07:00
4ad897392e
feat: better check toml empty
All checks were successful
build / build (push) Successful in 2m15s
build / trigger-build-image (push) Successful in 13s
2025-10-11 05:30:27 -07:00
d819847e8a
feat: skip on empty toml
All checks were successful
build / build (push) Successful in 2m26s
build / trigger-build-image (push) Successful in 12s
2025-10-09 00:30:03 -07:00
dd5f1123b4
feat: files.no-auto-import
All checks were successful
build / build (push) Successful in 2m2s
build / trigger-build-image (push) Successful in 9s
2025-10-08 05:59:47 -07:00
701541c032
fix: remove redundant code
All checks were successful
build / build (push) Successful in 1m54s
build / trigger-build-image (push) Successful in 8s
2025-10-07 20:44:16 -07:00
Boming Zhang
965253725d
fix: empty groups from repo.toml
All checks were successful
build / build (push) Successful in 2m9s
build / trigger-build-image (push) Successful in 9s
2025-10-06 15:36:29 -07:00
1633a3035b
chore: TODO
All checks were successful
build / build (push) Successful in 1m56s
build / trigger-build-image (push) Successful in 11s
2025-10-06 01:44:11 -07:00
23ddb5ef4f
feat: exit on no repo.toml found
All checks were successful
build / build (push) Successful in 1m57s
build / trigger-build-image (push) Successful in 8s
2025-10-04 21:45:58 -07:00
909eea8d5d
feat: support skip issue
All checks were successful
build / build (push) Successful in 1m30s
build / trigger-build-image (push) Successful in 8s
2025-10-04 18:43:53 -07:00
b69c42debc
feat: debug log joj3 summary
All checks were successful
build / build (push) Successful in 1m49s
build / trigger-build-image (push) Successful in 8s
2025-10-04 18:37:28 -07:00
371a17955a
feat: warn on debug = true
All checks were successful
build / build (push) Successful in 1m52s
build / trigger-build-image (push) Successful in 8s
2025-10-04 18:10:16 -07:00
930830b6f6
feat: debug parser
All checks were successful
build / build (push) Successful in 1m43s
build / trigger-build-image (push) Successful in 12s
2025-10-03 01:17:48 -07:00
32997cfdcb
feat: remove nested conf fields
All checks were successful
build / build (push) Successful in 2m2s
build / trigger-build-image (push) Successful in 11s
2025-09-27 01:05:45 -07:00
09f15f21ad
fix: specified case with base dir
All checks were successful
build / build (push) Successful in 2m0s
build / trigger-build-image (push) Successful in 11s
2025-09-26 01:26:42 -07:00
04839aecbd
feat: stages.base-case-dir
All checks were successful
build / build (push) Successful in 2m11s
build / trigger-build-image (push) Successful in 8s
2025-09-24 21:41:53 -07:00
046a553b56
fix: typo
Some checks failed
build / build (push) Successful in 2m4s
build / trigger-build-image (push) Failing after 9s
2025-09-24 03:26:47 -07:00
a6f931a879
feat: support groups
Some checks failed
build / build (push) Failing after 2m8s
build / trigger-build-image (push) Has been skipped
2025-09-24 03:20:21 -07:00
6f26d17656
feat: warn on multiple import
All checks were successful
build / build (push) Successful in 2m14s
build / trigger-build-image (push) Successful in 8s
2025-09-22 01:09:07 -07:00
Boming Zhang
8150d3d770
feat: better error msg & tests
All checks were successful
build / build (push) Successful in 2m9s
build / trigger-build-image (push) Successful in 12s
2025-09-17 15:54:27 -07:00
Boming Zhang
6795290d7c
feat: more strict models
All checks were successful
build / build (push) Successful in 2m2s
build / trigger-build-image (push) Successful in 12s
2025-09-17 14:26:09 -07:00
3f0fdd816b
feat: strict mode toml loader
All checks were successful
build / build (push) Successful in 2m16s
build / trigger-build-image (push) Successful in 9s
2025-09-17 01:13:51 -07:00
1a1d74a058
fix: setuptools-scm
All checks were successful
build / build (push) Successful in 2m3s
build / trigger-build-image (push) Successful in 10s
2025-09-14 17:45:45 -07:00
4a7295d008
fix: new codes & tests
Some checks failed
build / build (push) Failing after 1m52s
build / trigger-build-image (push) Has been skipped
2025-09-14 17:41:17 -07:00
571577ab70
fix: remove all old codes
Some checks failed
build / build (push) Failing after 2m3s
build / trigger-build-image (push) Has been skipped
2025-09-14 17:31:14 -07:00
0a625a76dd
feat: remove backward compatibility codes
Some checks failed
build / build (push) Failing after 2m12s
build / trigger-build-image (push) Has been skipped
2025-09-10 21:50:58 -07:00
7849cf9b3a
fix: immutable files fallback
All checks were successful
build / build (push) Successful in 2m26s
build / trigger-build-image (push) Successful in 10s
2025-07-31 06:16:04 -07:00
97eea108fb
feat: exit on immutable files not all covered
All checks were successful
build / build (push) Successful in 1m55s
build / trigger-build-image (push) Successful in 8s
2025-07-27 16:51:36 -07:00
44924d8c5f
feat: support git ref as scoreboard column
All checks were successful
build / build (push) Successful in 1m37s
build / trigger-build-image (push) Successful in 8s
2025-07-27 03:36:43 -07:00
e2d2ced3ae
feat: fallback for old immutable files
All checks were successful
build / build (push) Successful in 1m34s
build / trigger-build-image (push) Successful in 9s
2025-07-26 16:50:35 -07:00
eac9100146 docs: full toml sample (#20) (#27)
All checks were successful
build / build (push) Successful in 2m3s
build / trigger-build-image (push) Successful in 12s
Reviewed-on: https://focs.ji.sjtu.edu.cn/git/JOJ/JOJ3-config-generator/pulls/27
Co-authored-by: Boming Zhang <bomingzh@sjtu.edu.cn>
Co-committed-by: Boming Zhang <bomingzh@sjtu.edu.cn>
2025-07-08 04:38:47 +08:00
b3581a4071
chore: change fallback conf name to health check
All checks were successful
build / build (push) Successful in 2m9s
build / trigger-build-image (push) Successful in 11s
2025-07-03 09:36:24 -04:00
ea894953ae
feat: set max-total-score = 0 for fallback conf
All checks were successful
build / build (push) Successful in 2m12s
build / trigger-build-image (push) Successful in 9s
2025-07-01 11:21:56 -04:00
dcb2035be3
feat: try to find fallback conf with any name
All checks were successful
build / build (push) Successful in 1m55s
build / trigger-build-image (push) Successful in 10s
2025-07-01 08:39:11 -04:00
8b16214be4
feat: automatically create default fallback conf
All checks were successful
build / build (push) Successful in 3m42s
build / trigger-build-image (push) Successful in 10s
2025-07-01 08:29:12 -04:00
8f96115e6e
refactor: files.required -> health-check.required-files
All checks were successful
build / build (push) Successful in 1m40s
build / trigger-build-image (push) Successful in 10s
2025-07-01 00:35:42 -04:00
7e6ee1b63e
fix: rebase error
All checks were successful
build / build (push) Successful in 2m55s
build / trigger-build-image (push) Successful in 14s
2025-07-01 00:25:13 -04:00
dc7682a94f
refactor: move score, max-size, immutable-path to health-check
All checks were successful
build / build (push) Successful in 2m0s
build / trigger-build-image (push) Successful in 11s
2025-07-01 00:20:37 -04:00
ba720ebe3f
fix: detect cases
All checks were successful
build / build (push) Successful in 1m45s
build / trigger-build-image (push) Successful in 13s
2025-06-30 13:24:43 -04:00
88d864d49d
test: fix cases
All checks were successful
build / build (push) Successful in 1m56s
build / trigger-build-image (push) Successful in 14s
2025-06-30 06:30:34 -04:00
7c764ab2cb
feat: task.name -> name in task.toml
Some checks failed
build / build (push) Failing after 1m56s
build / trigger-build-image (push) Has been skipped
2025-06-30 06:23:41 -04:00
6a7c5eb5f8
feat: remove task.stage.skip 2025-06-29 14:28:11 -04:00
6769e19d83
chore: organize fields 2025-06-29 13:33:21 -04:00
26824fd93c
chore: clock time -> wall-clock time
All checks were successful
build / build (push) Successful in 2m16s
build / trigger-build-image (push) Successful in 9s
2025-06-26 11:15:33 -04:00
c7b02f17a2
fix: unnecessary fields check alias
All checks were successful
build / build (push) Successful in 1m50s
build / trigger-build-image (push) Successful in 11s
2025-06-26 08:28:16 -04:00
f4248a59a5
chore: result-detail.cpu alias
All checks were successful
build / build (push) Successful in 2m31s
build / trigger-build-image (push) Successful in 9s
2025-06-26 07:36:40 -04:00
8f5b23555c
chore: run time -> clock time 2025-06-26 07:36:09 -04:00
bb4f2b1364
feat: support limit.time
All checks were successful
build / build (push) Successful in 2m15s
build / trigger-build-image (push) Successful in 8s
2025-06-26 06:28:29 -04:00
10a5efb293
feat: set log path based on suffix
All checks were successful
build / build (push) Successful in 1m56s
build / trigger-build-image (push) Successful in 11s
2025-06-25 22:12:37 -04:00
c428d51706
feat: support --issue-label-exclusive
All checks were successful
build / build (push) Successful in 2m18s
build / trigger-build-image (push) Successful in 10s
2025-06-25 05:00:20 -04:00
4d3f77d6d0
test: use modern time.begin
All checks were successful
build / build (push) Successful in 2m30s
build / trigger-build-image (push) Successful in 12s
2025-06-25 03:49:08 -04:00
66551898be
test: fix cases
All checks were successful
build / build (push) Successful in 1m26s
build / trigger-build-image (push) Successful in 9s
2025-06-25 03:46:28 -04:00
1313a24690
fix: change default immutable_path to make it backward compatible
Some checks failed
build / trigger-build-image (push) Blocked by required conditions
build / build (push) Has been cancelled
2025-06-25 03:44:51 -04:00
087f62c304
feat: add unix timestamp back for backward compatibility
Some checks failed
build / build (push) Failing after 1m24s
build / trigger-build-image (push) Has been skipped
2025-06-25 02:42:37 -04:00
53395359cd
feat: files.import-map
All checks were successful
build / build (push) Successful in 1m36s
build / trigger-build-image (push) Successful in 7s
2025-06-22 05:55:09 -04:00
d785216396 feat: diff.output.xxx -> diff.xxx (#26)
All checks were successful
build / build (push) Successful in 1m38s
build / trigger-build-image (push) Successful in 8s
Reviewed-on: https://focs.ji.sjtu.edu.cn/git/JOJ/JOJ3-config-generator/pulls/26
Co-authored-by: Boming Zhang <bomingzh@sjtu.edu.cn>
Co-committed-by: Boming Zhang <bomingzh@sjtu.edu.cn>
2025-06-22 01:59:20 +08:00
8f1a82dc81
chore: remove unused field
All checks were successful
build / build (push) Successful in 3m11s
build / trigger-build-image (push) Successful in 18s
2025-06-21 12:25:58 -04:00
a49bc19b1e
chore: file_name -> filename
All checks were successful
build / build (push) Successful in 3m41s
build / trigger-build-image (push) Successful in 20s
2025-06-21 12:07:09 -04:00
1b4637d01c
chore: sort glob result for stable output
All checks were successful
build / build (push) Successful in 1m33s
build / trigger-build-image (push) Successful in 10s
2025-06-20 17:25:09 -04:00
875089cabd
fix: pdm run test
Some checks failed
build / build (push) Failing after 1m55s
build / trigger-build-image (push) Has been skipped
2025-06-20 17:18:00 -04:00
8d52febbc5
feat: new immutable dir (#25) 2025-06-20 17:07:24 -04:00
fd7d09e7b2
fix: more intuitive default score
All checks were successful
build / build (push) Successful in 2m4s
build / trigger-build-image (push) Successful in 9s
2025-06-20 11:54:51 -04:00
29952a9d2d
feat: support auto scoreboard name
All checks were successful
build / build (push) Successful in 1m56s
build / trigger-build-image (push) Successful in 9s
2025-06-20 04:01:45 -04:00
2bcf4f8c60
feat: support issue label config in repo.toml
All checks were successful
build / build (push) Successful in 1m49s
build / trigger-build-image (push) Successful in 11s
2025-06-20 03:40:16 -04:00
e2dc094263
feat: support penalty config
All checks were successful
build / build (push) Successful in 2m2s
build / trigger-build-image (push) Successful in 8s
2025-06-19 06:43:29 -04:00
42bbf3ce39
feat: warn on immutable file not found
All checks were successful
build / build (push) Successful in 1m42s
build / trigger-build-image (push) Successful in 9s
2025-06-18 10:14:29 -04:00
c6e2c63024
fix: cli time format
All checks were successful
build / build (push) Successful in 2m2s
build / trigger-build-image (push) Successful in 8s
2025-06-18 09:19:52 -04:00
cae592c5cf
test: update cases 2025-06-18 09:17:14 -04:00
52f82a4afc
feat: time.begin and time.end for teapot time check
All checks were successful
build / build (push) Successful in 1m52s
build / trigger-build-image (push) Successful in 8s
2025-06-18 09:13:34 -04:00
d8073e4eb6
fix: total score overwrite
All checks were successful
build / build (push) Successful in 1m52s
build / trigger-build-image (push) Successful in 10s
2025-06-17 02:15:10 -04:00
0b39aa7112
test: update cases 2025-06-17 02:05:40 -04:00
b46bc950f7
fix: optional max-total-score in repo.toml
Some checks failed
build / build (push) Failing after 1m48s
build / trigger-build-image (push) Has been skipped
2025-06-17 01:56:15 -04:00
340ba5d0c5
feat: more fields in result detail parser
All checks were successful
build / build (push) Successful in 1m54s
build / trigger-build-image (push) Successful in 13s
2025-06-17 01:42:25 -04:00
ed43414b86
refactor: simplify group str generation
All checks were successful
build / build (push) Successful in 1m51s
build / trigger-build-image (push) Successful in 8s
2025-06-16 10:09:29 -04:00
367a79850c
feat: support score in keyword parsers
All checks were successful
build / build (push) Successful in 1m47s
build / trigger-build-image (push) Successful in 10s
2025-06-15 10:49:54 -04:00
5404313199
feat: move teapot settings to repo.toml
All checks were successful
build / build (push) Successful in 1m43s
build / trigger-build-image (push) Successful in 8s
2025-06-15 02:05:15 -04:00
c6b833fbd5
fix: separated result status models
All checks were successful
build / build (push) Successful in 2m11s
build / trigger-build-image (push) Successful in 9s
2025-06-14 05:52:06 -04:00
b14d83c37b
fix: make it work for joj3-forge-convert
All checks were successful
build / build (push) Successful in 1m43s
build / trigger-build-image (push) Successful in 9s
2025-06-14 05:28:41 -04:00
2c1ad47f14
feat: get grading repo name from cwd
Some checks failed
build / build (push) Failing after 1m47s
build / trigger-build-image (push) Has been skipped
2025-06-14 05:21:35 -04:00
74 changed files with 4575 additions and 2199 deletions

View File

@ -23,9 +23,9 @@ jobs:
- name: PDM install dependencies - name: PDM install dependencies
run: | run: |
pdm install pdm install
- name: Lint - name: All
run: | run: |
pdm run lint pdm run all
- name: Run - name: Run
run: | run: |
pdm run app --help pdm run app --help

View File

@ -17,7 +17,7 @@ repos:
- id: yamlfmt - id: yamlfmt
exclude: '^tests/' exclude: '^tests/'
- repo: https://github.com/pdm-project/pdm - repo: https://github.com/pdm-project/pdm
rev: 2.19.2 rev: 2.25.9
hooks: hooks:
- id: pdm-lock-check - id: pdm-lock-check
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy

View File

@ -1,12 +1,18 @@
import os import os
from typing import Dict from typing import Dict
from joj3_config_generator.models import answer, joj1, repo, result, task from joj3_config_generator.models import answer, common, joj1, repo, result, task
from joj3_config_generator.models.const import ACTOR_CSV_PATH, JOJ3_LOG_PATH from joj3_config_generator.models.const import (
ACTOR_CSV_PATH,
JOJ3_LOG_BASE_PATH,
JOJ3_LOG_FILENAME,
TEAPOT_CONFIG_ROOT,
)
from joj3_config_generator.transformers.answer import get_task_conf_from_answers from joj3_config_generator.transformers.answer import get_task_conf_from_answers
from joj3_config_generator.transformers.joj1 import get_task_conf_from_joj1 from joj3_config_generator.transformers.joj1 import get_task_conf_from_joj1
from joj3_config_generator.transformers.repo import ( from joj3_config_generator.transformers.repo import (
get_health_check_stage, get_health_check_stage,
get_teapot_env,
get_teapot_post_stage, get_teapot_post_stage,
) )
from joj3_config_generator.transformers.task import get_conf_stage from joj3_config_generator.transformers.task import get_conf_stage
@ -23,31 +29,79 @@ def convert_joj1_conf(joj1_conf: joj1.Config) -> task.Config:
def convert_joj3_conf(repo_conf: repo.Config, task_conf: task.Config) -> result.Config: def convert_joj3_conf(repo_conf: repo.Config, task_conf: task.Config) -> result.Config:
# Create the base ResultConf object # Create the base ResultConf object
result_conf = result.Config( result_conf = result.Config(
name=task_conf.task.name, name=task_conf.name,
# exact folder difference specified by type # exact folder difference specified by type
log_path=str(JOJ3_LOG_PATH), log_path=str(JOJ3_LOG_BASE_PATH / task_conf.suffix / JOJ3_LOG_FILENAME),
expire_unix_timestamp=int(task_conf.release.end_time.timestamp()),
effective_unix_timestamp=int(task_conf.release.begin_time.timestamp()),
actor_csv_path=str(ACTOR_CSV_PATH), # students.csv position actor_csv_path=str(ACTOR_CSV_PATH), # students.csv position
max_total_score=( sandbox_token=repo_conf.sandbox_token,
repo_conf.max_total_score
if not task_conf.max_total_score
else task_conf.max_total_score
),
stage=result.Stage(sandbox_token=repo_conf.sandbox_token),
) )
current_test = os.environ.get("PYTEST_CURRENT_TEST") is not None current_test = os.environ.get("PYTEST_CURRENT_TEST") is not None
# Construct health check stage # Construct health check stage
if not repo_conf.force_skip_health_check_on_test or not current_test: if not repo_conf.force_skip_health_check_on_test or not current_test:
result_conf.stage.stages.append(get_health_check_stage(repo_conf, task_conf)) result_conf.stages.append(get_health_check_stage(repo_conf, task_conf))
cached: Dict[str, None] = {} cached: Dict[str, None] = {}
# Convert each stage in the task configuration # Convert each stage in the task configuration
for task_stage in task_conf.stages: for task_stage in task_conf.stages:
result_conf.stage.stages.append(get_conf_stage(task_conf, task_stage, cached)) result_conf.stages.append(get_conf_stage(task_conf, task_stage, cached))
if not repo_conf.force_skip_teapot_on_test or not current_test: if not repo_conf.force_skip_teapot_on_test or not current_test:
result_conf.stage.post_stages.append( result_conf.post_stages.append(get_teapot_post_stage(repo_conf, task_conf))
get_teapot_post_stage(repo_conf, task_conf)
)
return result_conf return result_conf
def create_joj3_convert_failure_conf() -> result.Config:
result_conf = result.Config(
name="Config generation failure",
log_path=str(JOJ3_LOG_BASE_PATH / JOJ3_LOG_FILENAME),
)
result_conf.stages.append(
result.StageDetail(
name="Error Message",
executor=result.Executor(name="dummy"),
parsers=[
result.Parser(
name="dummy",
with_=result.DummyConfig(
comment="Config generation failure. Contact teaching team to check course-joj repo actions for details.",
force_quit=True,
),
)
],
)
)
result_conf.post_stages.append(
result.StageDetail(
name="teapot",
executor=result.Executor(
name="local",
with_=result.ExecutorWith(
default=result.Cmd(
args=[
"/usr/local/bin/joint-teapot",
"joj3-all-env",
str(TEAPOT_CONFIG_ROOT / "teapot.env"),
"--skip-scoreboard",
"--skip-failed-table",
],
env=get_teapot_env(),
cpu_limit=common.Time("30s"),
clock_limit=common.Time("60s"),
),
cases=[],
),
),
parsers=[
result.Parser(
name="log",
with_=result.MsgConfig(
filename="stderr",
msg="joint-teapot stderr",
level=0,
),
),
result.Parser(name="debug"),
],
)
)
return result_conf

View File

@ -5,13 +5,23 @@ from typing import Any, Dict, Tuple, Type, cast
import inquirer import inquirer
import tomli import tomli
import yaml import yaml
from pydantic import BaseModel from pydantic import AliasChoices, BaseModel, ValidationError
from joj3_config_generator.models import answer, joj1, repo, task from joj3_config_generator.models import answer, joj1, repo, task
from joj3_config_generator.models.common import Memory, Time from joj3_config_generator.models.common import Memory, Time
from joj3_config_generator.utils.logger import logger from joj3_config_generator.utils.logger import logger
def is_toml_empty(toml_path: Path) -> bool:
if toml_path.stat().st_size == 0:
return True
try:
data = tomli.loads(toml_path.read_text())
return not data
except tomli.TOMLDecodeError:
return False
def load_joj3_task_toml_answers() -> answer.Answers: def load_joj3_task_toml_answers() -> answer.Answers:
name = inquirer.text("What's the task name?", default="hw0") name = inquirer.text("What's the task name?", default="hw0")
language = inquirer.list_input( language = inquirer.list_input(
@ -83,8 +93,16 @@ def load_joj3_toml(
f"{current_path}.{field_name}" if current_path else field_name f"{current_path}.{field_name}" if current_path else field_name
) )
toml_field_name = field_name toml_field_name = field_name
if field_info.alias in input_dict: if field_info.validation_alias:
toml_field_name = field_info.alias if isinstance(field_info.validation_alias, str):
if field_info.validation_alias in input_dict:
toml_field_name = field_info.validation_alias
elif isinstance(field_info.validation_alias, AliasChoices):
for choice in field_info.validation_alias.choices:
if choice in input_dict:
if isinstance(choice, str):
toml_field_name = choice
break
if toml_field_name not in input_dict: if toml_field_name not in input_dict:
continue continue
toml_value = input_dict[toml_field_name] toml_value = input_dict[toml_field_name]
@ -158,10 +176,22 @@ def load_joj3_toml(
repo_obj = tomli.loads(repo_toml_path.read_text()) repo_obj = tomli.loads(repo_toml_path.read_text())
task_obj = tomli.loads(task_toml_path.read_text()) task_obj = tomli.loads(task_toml_path.read_text())
try:
repo_conf = repo.Config(**repo_obj) repo_conf = repo.Config(**repo_obj)
except ValidationError as e:
logger.error(
f"Error parsing {repo_toml_path}, most likely to be unknown fields, check the latest sample toml carefully:\n{e}"
)
raise
repo_conf.root = root_path repo_conf.root = root_path
repo_conf.path = repo_toml_path.relative_to(root_path) repo_conf.path = repo_toml_path.relative_to(root_path)
try:
task_conf = task.Config(**task_obj) task_conf = task.Config(**task_obj)
except ValidationError as e:
logger.error(
f"Error parsing {task_toml_path}, most likely to be unknown fields, check the latest sample toml carefully:\n{e}"
)
raise
task_conf.root = root_path task_conf.root = root_path
task_conf.path = task_toml_path.relative_to(root_path) task_conf.path = task_toml_path.relative_to(root_path)
check_unnecessary_fields(repo.Config, repo_obj, repo_toml_path) check_unnecessary_fields(repo.Config, repo_obj, repo_toml_path)

View File

@ -10,9 +10,11 @@ from joj3_config_generator import get_version
from joj3_config_generator.generator import ( from joj3_config_generator.generator import (
convert_joj1_conf, convert_joj1_conf,
convert_joj3_conf, convert_joj3_conf,
create_joj3_convert_failure_conf,
create_joj3_task_conf, create_joj3_task_conf,
) )
from joj3_config_generator.loader import ( from joj3_config_generator.loader import (
is_toml_empty,
load_joj1_yaml, load_joj1_yaml,
load_joj3_task_toml_answers, load_joj3_task_toml_answers,
load_joj3_toml, load_joj3_toml,
@ -94,16 +96,30 @@ def convert(
""" """
app.pretty_exceptions_enable = False app.pretty_exceptions_enable = False
logger.info(f"Converting files in {root.absolute()}") logger.info(f"Converting files in {root.absolute()}")
error_json_paths = []
is_json_generated = False
for repo_toml_path in root.glob("**/repo.toml"): for repo_toml_path in root.glob("**/repo.toml"):
if not any(p != repo_toml_path for p in repo_toml_path.parent.glob("*.toml")):
fallback_toml_path = repo_toml_path.parent / "conf.toml"
if not fallback_toml_path.exists():
fallback_toml_path.write_text(
'name = "health check"\nmax-total-score = 0\n'
)
for task_toml_path in repo_toml_path.parent.glob("**/*.toml"): for task_toml_path in repo_toml_path.parent.glob("**/*.toml"):
if repo_toml_path == task_toml_path: if repo_toml_path == task_toml_path:
continue continue
if is_toml_empty(task_toml_path):
logger.info(f"Skipping empty task toml file {task_toml_path}")
continue
toml_name = task_toml_path.name.removesuffix(".toml") toml_name = task_toml_path.name.removesuffix(".toml")
result_json_path = task_toml_path.parent / f"{toml_name}.json" result_json_path = task_toml_path.parent / f"{toml_name}.json"
logger.info( logger.info(
f"Converting {repo_toml_path} & {task_toml_path} to {result_json_path}" f"Converting {repo_toml_path} & {task_toml_path} to {result_json_path}"
) )
repo_conf, task_conf = load_joj3_toml(root, repo_toml_path, task_toml_path) try:
repo_conf, task_conf = load_joj3_toml(
root, repo_toml_path, task_toml_path
)
result_model = convert_joj3_conf(repo_conf, task_conf) result_model = convert_joj3_conf(repo_conf, task_conf)
result_dict = result_model.model_dump( result_dict = result_model.model_dump(
mode="json", by_alias=True, exclude_none=True mode="json", by_alias=True, exclude_none=True
@ -111,3 +127,23 @@ def convert(
with result_json_path.open("w", newline="") as result_file: with result_json_path.open("w", newline="") as result_file:
json.dump(result_dict, result_file, ensure_ascii=False, indent=4) json.dump(result_dict, result_file, ensure_ascii=False, indent=4)
result_file.write("\n") result_file.write("\n")
is_json_generated = True
except Exception:
error_json_paths.append(result_json_path)
continue
if error_json_paths:
result_model = create_joj3_convert_failure_conf()
result_dict = result_model.model_dump(
mode="json", by_alias=True, exclude_none=True
)
for error_json_path in error_json_paths:
with error_json_path.open("w", newline="") as result_file:
json.dump(result_dict, result_file, ensure_ascii=False, indent=4)
result_file.write("\n")
logger.error(
f"Failed to convert {len(error_json_paths)} file(s): {', '.join(str(json_path) for json_path in error_json_paths)}. Check previous errors for details."
)
raise typer.Exit(code=1)
if not is_json_generated:
logger.error("No repo.toml files found to convert.")
raise typer.Exit(code=1)

View File

@ -1,6 +1,7 @@
from typing import Union from typing import Union
import humanfriendly import humanfriendly
from pydantic import BaseModel, ConfigDict
class Memory(int): class Memory(int):
@ -17,3 +18,7 @@ class Time(int):
parsed = humanfriendly.parse_timespan(value) * 1_000_000_000 # ns parsed = humanfriendly.parse_timespan(value) * 1_000_000_000 # ns
return super().__new__(cls, round(parsed)) return super().__new__(cls, round(parsed))
return super().__new__(cls, value) return super().__new__(cls, value)
class StrictBaseModel(BaseModel):
model_config = ConfigDict(extra="forbid")

View File

@ -13,6 +13,7 @@ DEFAULT_PATH_ENV = "PATH=/usr/bin:/bin:/usr/local/bin"
JOJ3_CONFIG_ROOT = PurePosixPath("/home/tt/.config/joj") JOJ3_CONFIG_ROOT = PurePosixPath("/home/tt/.config/joj")
TEAPOT_CONFIG_ROOT = PurePosixPath("/home/tt/.config/teapot") TEAPOT_CONFIG_ROOT = PurePosixPath("/home/tt/.config/teapot")
CACHE_ROOT = PurePosixPath("/home/tt/.cache") CACHE_ROOT = PurePosixPath("/home/tt/.cache")
JOJ3_LOG_PATH = CACHE_ROOT / "joj3" / "joj3.log" JOJ3_LOG_BASE_PATH = CACHE_ROOT / "joj3"
JOJ3_LOG_FILENAME = "joj3.log"
TEAPOT_LOG_PATH = CACHE_ROOT / "joint-teapot-debug.log" TEAPOT_LOG_PATH = CACHE_ROOT / "joint-teapot-debug.log"
ACTOR_CSV_PATH = JOJ3_CONFIG_ROOT / "students.csv" ACTOR_CSV_PATH = JOJ3_CONFIG_ROOT / "students.csv"

View File

@ -1,16 +1,18 @@
import socket import os
from pathlib import Path from pathlib import Path
from typing import List from typing import Any, List
from pydantic import AliasChoices, BaseModel, Field from pydantic import AliasChoices, Field, field_validator, model_validator
from joj3_config_generator.models.common import Memory, StrictBaseModel
class Files(BaseModel): class Files(StrictBaseModel):
required: List[str] = [] required: List[str] = []
immutable: List[str] = [] immutable: List[str] = []
class Groups(BaseModel): class Groups(StrictBaseModel):
name: List[str] = [] name: List[str] = []
max_count: List[int] = Field( max_count: List[int] = Field(
[], validation_alias=AliasChoices("max-count", "max_count") [], validation_alias=AliasChoices("max-count", "max_count")
@ -18,44 +20,99 @@ class Groups(BaseModel):
time_period_hour: List[int] = Field( time_period_hour: List[int] = Field(
[], validation_alias=AliasChoices("time-period-hour", "time_period_hour") [], validation_alias=AliasChoices("time-period-hour", "time_period_hour")
) )
ignore_submitter: bool = Field(
False, validation_alias=AliasChoices("ignore-submitter", "ignore_submitter")
)
class Config(BaseModel): class Label(StrictBaseModel):
max_size: float = Field( name: str = "Kind/Testing"
10, ge=0, validation_alias=AliasChoices("max-size", "max_size") color: str = "#795548"
exclusive: bool = False
class Issue(StrictBaseModel):
label: Label = Label()
show_submitter: bool = Field(
True, validation_alias=AliasChoices("show-submitter", "show_submitter")
) )
files: Files = Files()
sandbox_token: str = Field(
"", validation_alias=AliasChoices("sandbox-token", "sandbox_token") class HealthCheck(StrictBaseModel):
score: int = 0
max_size: int = Field(
Memory("10m"), validation_alias=AliasChoices("max-size", "max_size")
) )
max_total_score: int = Field( immutable_path: Path = Field(
100, validation_alias=AliasChoices("max-total-score", "max_total_score") Path("immutable"),
validation_alias=AliasChoices("immutable-path", "immutable_path"),
) )
required_files: List[str] = Field(
[], validation_alias=AliasChoices("required-files", "required_files")
)
whitelisted_chars: str = Field(
"", validation_alias=AliasChoices("whitelisted-chars", "whitelisted_chars")
)
@field_validator("max_size", mode="before")
@classmethod
def ensure_mem_type(cls, v: Any) -> Memory:
if isinstance(v, str):
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_chars(cls, chars: str) -> str:
for c in chars:
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)
path: Path = Field(Path("repo.toml"), exclude=True)
force_skip_health_check_on_test: bool = Field( force_skip_health_check_on_test: bool = Field(
False, False,
validation_alias=AliasChoices( validation_alias=AliasChoices(
"force-skip-health-check-on-test", "force_skip_health_check_on_test" "force-skip-health-check-on-test", "force_skip_health_check_on_test"
), ),
exclude=True,
) )
force_skip_teapot_on_test: bool = Field( force_skip_teapot_on_test: bool = Field(
False, False,
validation_alias=AliasChoices( validation_alias=AliasChoices(
"force-skip-teapot-on-test", "force_skip_teapot_on_test" "force-skip-teapot-on-test", "force_skip_teapot_on_test"
), ),
exclude=True,
) )
groups: Groups = Groups()
root: Path = Path(".")
path: Path = Path("repo.toml")
grading_repo_name: str = Field( grading_repo_name: str = Field(
f"{socket.gethostname().split('-')[0]}-joj", "",
validation_alias=AliasChoices("grading-repo-name", "grading_repo_name"), validation_alias=AliasChoices("grading-repo-name", "grading_repo_name"),
) )
health_check_score: int = Field( sandbox_token: str = Field(
0, validation_alias=AliasChoices("health-check-score", "health_check_score") "", validation_alias=AliasChoices("sandbox-token", "sandbox_token")
) )
submitter_in_issue_title: bool = Field( max_total_score: int = Field(
True, 100, validation_alias=AliasChoices("max-total-score", "max_total_score")
validation_alias=AliasChoices(
"submitter-in-issue-title", "submitter_in_issue_title"
),
) )
groups: Groups = Groups()
issue: Issue = Issue()
health_check: HealthCheck = Field(
HealthCheck(), validation_alias=AliasChoices("health-check", "health_check")
)
@model_validator(mode="after")
def set_grading_repo_name_from_cwd(self) -> "Config":
if not self.grading_repo_name:
course_env = os.getenv("COURSE")
if course_env:
self.grading_repo_name = f"{course_env}-joj"
else:
self.grading_repo_name = Path.cwd().name
return self

View File

@ -146,12 +146,15 @@ class Parser(BaseModel):
class StageDetail(BaseModel): class StageDetail(BaseModel):
name: str name: str
group: str = "" groups: List[str] = []
executor: Executor executor: Executor
parsers: List[Parser] parsers: List[Parser]
class Stage(BaseModel): class Config(BaseModel):
name: str = ""
log_path: str = Field("", serialization_alias="logPath")
actor_csv_path: str = Field("", serialization_alias="actorCsvPath")
sandbox_exec_server: str = Field( sandbox_exec_server: str = Field(
"172.17.0.1:5051", serialization_alias="sandboxExecServer" "172.17.0.1:5051", serialization_alias="sandboxExecServer"
) )
@ -159,24 +162,18 @@ class Stage(BaseModel):
output_path: str = Field( output_path: str = Field(
"/tmp/joj3_result.json", serialization_alias="outputPath" "/tmp/joj3_result.json", serialization_alias="outputPath"
) # nosec: B108 ) # nosec: B108
stages: List[StageDetail] = []
pre_stages: List[StageDetail] = Field([], serialization_alias="preStages") pre_stages: List[StageDetail] = Field([], serialization_alias="preStages")
stages: List[StageDetail] = []
post_stages: List[StageDetail] = Field([], serialization_alias="postStages") post_stages: List[StageDetail] = Field([], serialization_alias="postStages")
class Config(BaseModel):
name: str = ""
log_path: str = Field("", serialization_alias="logPath")
expire_unix_timestamp: int = Field(-1, serialization_alias="expireUnixTimestamp")
effective_unix_timestamp: int = Field(
-1, serialization_alias="effectiveUnixTimestamp"
)
actor_csv_path: str = Field("", serialization_alias="actorCsvPath")
max_total_score: int = Field(100, serialization_alias="maxTotalScore")
stage: Stage
class DummyConfig(BaseModel): class DummyConfig(BaseModel):
score: int = 0
comment: Optional[str] = None
force_quit: Optional[bool] = Field(False, serialization_alias="forceQuit")
class ResultStatusConfig(BaseModel):
score: int = 0 score: int = 0
comment: Optional[str] = None comment: Optional[str] = None
force_quit_on_not_accepted: Optional[bool] = Field( force_quit_on_not_accepted: Optional[bool] = Field(
@ -186,7 +183,7 @@ class DummyConfig(BaseModel):
class DiffOutputConfig(BaseModel): class DiffOutputConfig(BaseModel):
score: int = 100 score: int = 100
file_name: str = Field("", serialization_alias="fileName") filename: str = Field("", serialization_alias="filename")
answer_path: str = Field("", serialization_alias="answerPath") answer_path: str = Field("", serialization_alias="answerPath")
compare_space: bool = Field(False, serialization_alias="compareSpace") compare_space: bool = Field(False, serialization_alias="compareSpace")
always_hide: bool = Field(False, serialization_alias="alwaysHide") always_hide: bool = Field(False, serialization_alias="alwaysHide")
@ -217,6 +214,7 @@ class KeywordConfig(BaseModel):
class KeywordMatchConfig(BaseModel): class KeywordMatchConfig(BaseModel):
score: int = 0
matches: List[KeywordConfig] = [] matches: List[KeywordConfig] = []
@ -234,7 +232,9 @@ class DiffConfig(BaseModel):
class MsgConfig(BaseModel): class MsgConfig(BaseModel):
filename: str = "stdout"
msg: str = "" msg: str = ""
level: int = 0
class ScoreConfig(BaseModel): class ScoreConfig(BaseModel):

View File

@ -1,20 +1,15 @@
import re
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Type from typing import Any, Dict, List, Optional, Type
from pydantic import ( from pydantic import AliasChoices, ConfigDict, Field, field_validator, model_validator
AliasChoices,
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
)
from joj3_config_generator.models.common import Memory, Time from joj3_config_generator.models.common import Memory, StrictBaseModel, Time
from joj3_config_generator.models.const import ( from joj3_config_generator.models.const import (
DEFAULT_CASE_SCORE, DEFAULT_CASE_SCORE,
DEFAULT_CLOCK_LIMIT_MULTIPLIER,
DEFAULT_CPU_LIMIT, DEFAULT_CPU_LIMIT,
DEFAULT_FILE_LIMIT, DEFAULT_FILE_LIMIT,
DEFAULT_MEMORY_LIMIT, DEFAULT_MEMORY_LIMIT,
@ -23,11 +18,11 @@ from joj3_config_generator.models.const import (
from joj3_config_generator.models.repo import Groups from joj3_config_generator.models.repo import Groups
class ParserResultDetail(BaseModel): class ParserResultDetail(StrictBaseModel):
cpu_time: bool = Field( cpu_time: bool = Field(
True, validation_alias=AliasChoices("cpu-time", "cpu_time") True, validation_alias=AliasChoices("cpu-time", "cpu_time", "cpu")
) # Display CPU time ) # Display CPU time
time: bool = True # Display run time time: bool = True # Display wall-clock time
mem: bool = True # Display memory usage mem: bool = True # Display memory usage
stdout: bool = False # Display stdout messages stdout: bool = False # Display stdout messages
stderr: bool = False # Display stderr messages stderr: bool = False # Display stderr messages
@ -38,19 +33,25 @@ class ParserResultDetail(BaseModel):
False, validation_alias=AliasChoices("proc-peak", "proc_peak") False, validation_alias=AliasChoices("proc-peak", "proc_peak")
) # Display peak process count ) # Display peak process count
error: bool = False # Display error messages error: bool = False # Display error messages
code_block: bool = Field(
True, validation_alias=AliasChoices("code-block", "code_block")
) # Display file in code block
max_length: int = Field(
2048, validation_alias=AliasChoices("max-length", "max_length")
) # Max output length of each file
class ParserFile(BaseModel): class ParserFile(StrictBaseModel):
name: str = "" name: str = ""
class ParserLog(BaseModel): class ParserLog(StrictBaseModel):
filename: str filename: str
msg: str = "" msg: str = ""
level: str = "" level: str = ""
class ParserDummy(BaseModel): class ParserDummy(StrictBaseModel):
comment: str = "" comment: str = ""
score: int = 0 score: int = 0
force_quit: bool = Field( force_quit: bool = Field(
@ -58,12 +59,21 @@ class ParserDummy(BaseModel):
) )
class ParserKeyword(BaseModel): class ParserResultStatus(StrictBaseModel):
comment: str = ""
score: int = 0
force_quit: bool = Field(
True, validation_alias=AliasChoices("force-quit", "force_quit")
)
class ParserKeyword(StrictBaseModel):
score: int = 0
keyword: List[str] = [] keyword: List[str] = []
weight: List[int] = [] weight: List[int] = []
class ParserDiffOutputs(BaseModel): class ParserDiffOutputs(StrictBaseModel):
score: int = 0 score: int = 0
ignore_spaces: bool = Field( ignore_spaces: bool = Field(
True, validation_alias=AliasChoices("ignore-spaces", "ignore_spaces") True, validation_alias=AliasChoices("ignore-spaces", "ignore_spaces")
@ -81,41 +91,79 @@ class ParserDiffOutputs(BaseModel):
) )
class ParserDiff(BaseModel): class ParserDiff(StrictBaseModel):
score: int = DEFAULT_CASE_SCORE
ignore_spaces: bool = Field(
True, validation_alias=AliasChoices("ignore-spaces", "ignore_spaces")
)
hide: bool = False
force_quit: bool = Field(
False, validation_alias=AliasChoices("force-quit", "force_quit")
)
max_length: int = Field(
2048, validation_alias=AliasChoices("max-length", "max_length")
)
max_lines: int = Field(50, validation_alias=AliasChoices("max-lines", "max_lines"))
hide_common_prefix: bool = Field(
False, validation_alias=AliasChoices("hide-common-prefix", "hide_common_prefix")
)
# remove below codes when migration is complete
output: ParserDiffOutputs = ParserDiffOutputs() output: ParserDiffOutputs = ParserDiffOutputs()
default_score: int = Field( default_score: int = Field(
DEFAULT_CASE_SCORE, DEFAULT_CASE_SCORE,
validation_alias=AliasChoices("default-score", "default_score"), validation_alias=AliasChoices("default-score", "default_score"),
) )
@model_validator(mode="after")
def copy_output_fields(self) -> "ParserDiff":
if "default_score" in self.model_fields_set:
self.score = self.default_score
if not isinstance(self.output, ParserDiffOutputs):
return self
for field_name, field_value in self.output.model_dump().items():
if field_name in self.output.model_fields_set and hasattr(self, field_name):
setattr(self, field_name, field_value)
return self
class StageFiles(BaseModel):
class StageFiles(StrictBaseModel):
import_: List[str] = Field([], validation_alias="import") import_: List[str] = Field([], validation_alias="import")
export: List[str] = [] export: List[str] = []
import_map: Dict[str, str] = Field(
{}, validation_alias=AliasChoices("import-map", "import_map")
)
no_auto_import: List[str] = Field(
[], validation_alias=AliasChoices("no-auto-import", "no_auto_import")
)
class Limit(BaseModel): class Limit(StrictBaseModel):
mem: int = DEFAULT_MEMORY_LIMIT mem: int = DEFAULT_MEMORY_LIMIT
cpu: int = DEFAULT_CPU_LIMIT cpu: int = DEFAULT_CPU_LIMIT
time: int = 0
stdout: int = DEFAULT_FILE_LIMIT stdout: int = DEFAULT_FILE_LIMIT
stderr: int = DEFAULT_FILE_LIMIT stderr: int = DEFAULT_FILE_LIMIT
proc: int = DEFAULT_PROC_LIMIT proc: int = DEFAULT_PROC_LIMIT
model_config = ConfigDict(validate_assignment=True) @field_validator("cpu", "time", mode="before")
@field_validator("cpu", mode="before")
@classmethod @classmethod
def ensure_time(cls, v: Any) -> Time: def ensure_time_type(cls, v: Any) -> Time:
if isinstance(v, str): if isinstance(v, str):
return Time(v) return Time(v)
raise ValueError("Must be a string") raise ValueError(f'Must be a string, e.g., "1s" or "100ms", but got {v}')
@field_validator("mem", "stdout", "stderr", mode="before") @field_validator("mem", "stdout", "stderr", mode="before")
@classmethod @classmethod
def ensure_mem(cls, v: Any) -> Memory: def ensure_mem_type(cls, v: Any) -> Memory:
if isinstance(v, str): if isinstance(v, str):
return Memory(v) return Memory(v)
raise ValueError("Must be a string") raise ValueError(f'Must be a string, e.g., "256m" or "1g", but got {v}')
@model_validator(mode="after")
def set_time_if_not_set(self) -> "Limit":
if self.time == 0:
self.time = DEFAULT_CLOCK_LIMIT_MULTIPLIER * self.cpu
return self
class Parser(str, Enum): class Parser(str, Enum):
@ -125,13 +173,14 @@ class Parser(str, Enum):
KEYWORD = "keyword" KEYWORD = "keyword"
RESULT_STATUS = "result-status" RESULT_STATUS = "result-status"
RESULT_DETAIL = "result-detail" RESULT_DETAIL = "result-detail"
DEBUG = "debug"
DUMMY = "dummy" DUMMY = "dummy"
FILE = "file" FILE = "file"
DIFF = "diff" DIFF = "diff"
ELF = "elf" ELF = "elf"
class Case(BaseModel): class Case(StrictBaseModel):
env: List[str] = [] env: List[str] = []
command: str = "" # Command to run command: str = "" # Command to run
files: StageFiles = StageFiles() files: StageFiles = StageFiles()
@ -141,18 +190,21 @@ class Case(BaseModel):
True, validation_alias=AliasChoices("copy-in-cwd", "copy_in_cwd") True, validation_alias=AliasChoices("copy-in-cwd", "copy_in_cwd")
) )
limit: Limit = Limit() limit: Limit = Limit()
score: int = 0
diff: ParserDiff = ParserDiff() diff: ParserDiff = ParserDiff()
class Stage(Case): class Stage(Case):
name: str = "" # stage name name: str = "" # stage name
skip: List[str] = [] groups: List[str] = [] # list of groups
base_case_dir: str = Field(
".", validation_alias=AliasChoices("base-case-dir", "base_case_dir")
) # base directory for finding cases
parsers: List[Parser] = [] # list of parsers parsers: List[Parser] = [] # list of parsers
dummy: ParserDummy = ParserDummy() dummy: ParserDummy = ParserDummy()
result_status: ParserDummy = Field( result_status: ParserResultStatus = Field(
ParserDummy(), validation_alias=AliasChoices("result-status", "result_status") ParserResultStatus(),
validation_alias=AliasChoices("result-status", "result_status"),
) )
keyword: ParserKeyword = ParserKeyword() keyword: ParserKeyword = ParserKeyword()
clangtidy: ParserKeyword = ParserKeyword() clangtidy: ParserKeyword = ParserKeyword()
@ -164,6 +216,7 @@ class Stage(Case):
validation_alias=AliasChoices("result-detail", "result_detail"), validation_alias=AliasChoices("result-detail", "result_detail"),
) )
file: ParserFile = ParserFile() file: ParserFile = ParserFile()
# diff: ParserDiff = ParserDiff() # inherited from Case
cases: Dict[str, Case] = {} cases: Dict[str, Case] = {}
@ -184,7 +237,7 @@ class Stage(Case):
return values return values
class Release(BaseModel): class Release(StrictBaseModel):
end_time: datetime = Field( end_time: datetime = Field(
datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
validation_alias=AliasChoices("end-time", "end_time"), validation_alias=AliasChoices("end-time", "end_time"),
@ -195,17 +248,61 @@ class Release(BaseModel):
) # timestamp = 0, no begin time ) # timestamp = 0, no begin time
class Task(BaseModel): class SubmissionTime(StrictBaseModel):
name: str = "unknown" begin: Optional[datetime] = None
end: Optional[datetime] = None
class Config(BaseModel): class Penalties(StrictBaseModel):
root: Path = Path(".") hours: List[float] = []
path: Path = Path("task.toml") factors: List[float] = []
task: Task = Task() # Task name (e.g., hw3 ex5)
release: Release = Release() # Release configuration
stages: List[Stage] = [] # list of stage configurations class Issue(StrictBaseModel):
groups: Groups = Groups() skip: bool = False
max_total_score: int = Field(
100, validation_alias=AliasChoices("max-total-score", "max_total_score")
class Grading(StrictBaseModel):
skip_scoreboard: bool = Field(
False, validation_alias=AliasChoices("skip-scoreboard", "skip_scoreboard")
) )
skip_failed_table: bool = Field(
False, validation_alias=AliasChoices("skip-failed-table", "skip_failed_table")
)
class Config(StrictBaseModel):
root: Path = Field(Path("."), exclude=True)
path: Path = Field(Path("task.toml"), exclude=True)
suffix: str = Field("", exclude=True)
name: str = "unknown" # Task name (e.g., hw3 ex5)
max_total_score: Optional[int] = Field(
None, validation_alias=AliasChoices("max-total-score", "max_total_score")
)
issue: Issue = Issue()
grading: Grading = Grading()
scoreboard: str = "scoreboard.csv"
scoreboard_column_by_ref: bool = Field(
False,
validation_alias=AliasChoices(
"scoreboard-column-by-ref", "scoreboard_column_by_ref"
),
)
time: SubmissionTime = SubmissionTime() # Valid time configuration
release: Release = Release() # Release configuration
groups: Groups = Groups()
penalties: Penalties = Penalties()
stages: List[Stage] = [] # list of stage configurations
@model_validator(mode="after")
def set_suffix(self) -> "Config":
if not self.suffix:
self.suffix = re.split(r"[-_/\s]+", self.name)[0]
return self
@model_validator(mode="after")
def set_scoreboard(self) -> "Config":
if self.scoreboard == "auto":
self.scoreboard = f"scoreboard-{self.suffix}.csv"
return self

View File

@ -9,16 +9,16 @@ def get_task_conf_from_answers(answers: answer.Answers) -> task.Config:
if answers.template_file_content: if answers.template_file_content:
toml_dict = tomli.loads(answers.template_file_content) toml_dict = tomli.loads(answers.template_file_content)
return task.Config( return task.Config(
task=task.Task(name=answers.name), name=answers.name,
stages=toml_dict["stages"], stages=toml_dict["stages"],
) )
language = answers.language language = answers.language
transformer_dict = get_transformer_dict() transformer_dict = get_transformer_dict()
if language not in transformer_dict: if language not in transformer_dict:
return task.Config(task=task.Task(name=answers.name), stages=[]) return task.Config(name=answers.name, stages=[])
transformer = transformer_dict[language] transformer = transformer_dict[language]
stages = transformer(language) stages = transformer(language)
return task.Config(task=task.Task(name=answers.name), stages=stages) return task.Config(name=answers.name, stages=stages)
def get_transformer_dict() -> Dict[ def get_transformer_dict() -> Dict[

View File

@ -1,9 +1,21 @@
import hashlib import hashlib
from pathlib import Path from pathlib import Path
from typing import List from typing import List, Tuple
from joj3_config_generator.models import common, repo, result, task from joj3_config_generator.models import common, repo, result, task
from joj3_config_generator.models.const import TEAPOT_CONFIG_ROOT, TEAPOT_LOG_PATH from joj3_config_generator.models.const import (
CACHE_ROOT,
TEAPOT_CONFIG_ROOT,
TEAPOT_LOG_PATH,
)
from joj3_config_generator.utils.logger import logger
def get_teapot_env() -> List[str]:
return [
f"REPOS_DIR={CACHE_ROOT}",
f"LOG_FILE_PATH={TEAPOT_LOG_PATH}",
]
def get_teapot_post_stage( def get_teapot_post_stage(
@ -17,13 +29,44 @@ def get_teapot_post_stage(
repo_conf.grading_repo_name, repo_conf.grading_repo_name,
"--max-total-score", "--max-total-score",
( (
str(repo_conf.max_total_score) str(task_conf.max_total_score)
if not task_conf.max_total_score if task_conf.max_total_score is not None
else str(task_conf.max_total_score) else str(repo_conf.max_total_score)
), ),
"--issue-label-name",
repo_conf.issue.label.name,
"--issue-label-color",
repo_conf.issue.label.color,
"--scoreboard-filename",
task_conf.scoreboard,
] ]
if not repo_conf.submitter_in_issue_title: if task_conf.scoreboard_column_by_ref:
args.append("--scoreboard-column-by-ref")
if task_conf.issue.skip:
args.append("--skip-result-issue")
if task_conf.grading.skip_scoreboard:
args.append("--skip-scoreboard")
if task_conf.grading.skip_failed_table:
args.append("--skip-failed-table")
if repo_conf.issue.label.exclusive:
args.append("--issue-label-exclusive")
if not repo_conf.issue.show_submitter:
args.append("--no-submitter-in-issue-title") args.append("--no-submitter-in-issue-title")
if task_conf.time.end:
args.extend(
[
"--end-time",
task_conf.time.end.strftime("%Y-%m-%dT%H:%M:%S"),
]
)
if task_conf.penalties.hours:
penalty_config = ",".join(
f"{hour}={factor}"
for hour, factor in zip(
task_conf.penalties.hours, task_conf.penalties.factors
)
)
args.extend(["--penalty-config", penalty_config])
stage_conf = result.StageDetail( stage_conf = result.StageDetail(
name="teapot", name="teapot",
@ -32,7 +75,7 @@ def get_teapot_post_stage(
with_=result.ExecutorWith( with_=result.ExecutorWith(
default=result.Cmd( default=result.Cmd(
args=args, args=args,
env=[f"LOG_FILE_PATH={TEAPOT_LOG_PATH}"], env=get_teapot_env(),
cpu_limit=common.Time("30s"), cpu_limit=common.Time("30s"),
clock_limit=common.Time("60s"), clock_limit=common.Time("60s"),
), ),
@ -40,22 +83,63 @@ def get_teapot_post_stage(
), ),
), ),
parsers=[ parsers=[
result.Parser(name="log", with_=result.MsgConfig(msg="joj3 summary")), result.Parser(
name="log",
with_=result.MsgConfig(
filename="stdout",
msg="joj3 summary",
level=-4,
),
),
result.Parser(
name="log",
with_=result.MsgConfig(
filename="stderr",
msg="joint-teapot stderr",
level=0,
),
),
result.Parser(name="debug"), result.Parser(name="debug"),
], ],
) )
return stage_conf return stage_conf
def get_check_lists(repo_conf: repo.Config) -> Tuple[List[str], List[str]]:
base_dir = (repo_conf.root / repo_conf.path).parent
immutable_dir = base_dir / repo_conf.health_check.immutable_path
file_sums = []
file_paths = []
for file_path in sorted(immutable_dir.glob("**/*")):
if file_path.is_dir():
continue
if not file_path.exists():
logger.warning(f"Immutable file not found: {file_path}")
continue
file_sums.append(calc_sha256sum(file_path))
file_paths.append(file_path.relative_to(immutable_dir).as_posix())
return file_sums, file_paths
def get_health_check_args(repo_conf: repo.Config) -> List[str]: def get_health_check_args(repo_conf: repo.Config) -> List[str]:
return [ file_sums, file_paths = get_check_lists(repo_conf)
args = [
"/usr/local/bin/repo-health-checker", "/usr/local/bin/repo-health-checker",
"-root=.", "-root=.",
f"-repoSize={str(repo_conf.max_size)}", f"-repoSize={str(repo_conf.health_check.max_size / 1024 / 1024)}", # B -> MB
*[f"-meta={meta}" for meta in repo_conf.files.required], *[f"-meta={meta}" for meta in repo_conf.health_check.required_files],
f"-checkFileSumList={','.join(get_hashes(repo_conf))}",
f"-checkFileNameList={','.join(repo_conf.files.immutable)}",
] ]
if repo_conf.health_check.whitelisted_chars:
args.append(
f"-whitelistedChars={','.join(list(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]: def get_teapot_check_args(repo_conf: repo.Config, task_conf: task.Config) -> List[str]:
@ -65,28 +149,34 @@ def get_teapot_check_args(repo_conf: repo.Config, task_conf: task.Config) -> Lis
str(TEAPOT_CONFIG_ROOT / "teapot.env"), str(TEAPOT_CONFIG_ROOT / "teapot.env"),
"--grading-repo-name", "--grading-repo-name",
repo_conf.grading_repo_name, repo_conf.grading_repo_name,
"--scoreboard-filename",
task_conf.scoreboard,
] ]
if repo_conf.groups.name: if repo_conf.groups.name or task_conf.groups.name:
group_config_str = ",".join( groups = task_conf.groups if task_conf.groups.name else repo_conf.groups
group_config = ",".join(
f"{name}={max_count}:{time_period}" f"{name}={max_count}:{time_period}"
for name, max_count, time_period in zip( for name, max_count, time_period in zip(
repo_conf.groups.name, groups.name,
repo_conf.groups.max_count, groups.max_count,
repo_conf.groups.time_period_hour, groups.time_period_hour,
) )
) )
if task_conf.groups.name: res.extend(["--group-config", group_config])
overwrite_group_config_str = ",".join( if task_conf.time.begin:
f"{name}={max_count}:{time_period}" res.extend(["--begin-time", task_conf.time.begin.strftime("%Y-%m-%dT%H:%M:%S")])
for name, max_count, time_period in zip( if task_conf.time.end:
task_conf.groups.name, res.extend(["--end-time", task_conf.time.end.strftime("%Y-%m-%dT%H:%M:%S")])
task_conf.groups.max_count, if task_conf.penalties.hours:
task_conf.groups.time_period_hour, penalty_config = ",".join(
f"{hour}={factor}"
for hour, factor in zip(
task_conf.penalties.hours, task_conf.penalties.factors
) )
) )
res.extend(["--group-config", overwrite_group_config_str]) res.extend(["--penalty-config", penalty_config])
else: if task_conf.groups.ignore_submitter:
res.extend(["--group-config", group_config_str]) res.append("--ignore-submitter")
return res return res
@ -95,12 +185,13 @@ def get_health_check_stage(
) -> result.StageDetail: ) -> result.StageDetail:
health_check_stage = result.StageDetail( health_check_stage = result.StageDetail(
name="Health Check", name="Health Check",
group="", groups=[],
executor=result.Executor( executor=result.Executor(
name="local", name="local",
with_=result.ExecutorWith( with_=result.ExecutorWith(
default=result.Cmd( default=result.Cmd(
cpu_limit=common.Time("10s"), clock_limit=common.Time("20s") cpu_limit=common.Time("10s"),
clock_limit=common.Time("20s"),
), ),
cases=[ cases=[
result.OptionalCmd( result.OptionalCmd(
@ -108,7 +199,7 @@ def get_health_check_stage(
), ),
result.OptionalCmd( result.OptionalCmd(
args=get_teapot_check_args(repo_conf, task_conf), args=get_teapot_check_args(repo_conf, task_conf),
env=[f"LOG_FILE_PATH={TEAPOT_LOG_PATH}"], env=get_teapot_env(),
), ),
], ],
), ),
@ -116,7 +207,7 @@ def get_health_check_stage(
parsers=[ parsers=[
result.Parser( result.Parser(
name="healthcheck", name="healthcheck",
with_=result.ScoreConfig(score=repo_conf.health_check_score), with_=result.ScoreConfig(score=repo_conf.health_check.score),
), ),
result.Parser(name="debug"), result.Parser(name="debug"),
], ],
@ -130,12 +221,3 @@ def calc_sha256sum(file_path: Path) -> str:
for byte_block in iter(lambda: f.read(64 * 1024), b""): for byte_block in iter(lambda: f.read(64 * 1024), b""):
sha256_hash.update(byte_block) sha256_hash.update(byte_block)
return sha256_hash.hexdigest() return sha256_hash.hexdigest()
def get_hashes(repo_conf: repo.Config) -> List[str]:
base_dir = (repo_conf.root / repo_conf.path).parent
immutable_dir = base_dir / "immutable_files"
immutable_files = [
immutable_dir / Path(file).name for file in repo_conf.files.immutable
]
return [calc_sha256sum(file) for file in immutable_files]

View File

@ -2,15 +2,13 @@ import re
import shlex import shlex
from functools import partial from functools import partial
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from natsort import natsorted
from joj3_config_generator.models import result, task from joj3_config_generator.models import result, task
from joj3_config_generator.models.common import Memory, Time from joj3_config_generator.models.common import StrictBaseModel
from joj3_config_generator.models.const import ( from joj3_config_generator.models.const import DEFAULT_PATH_ENV, JOJ3_CONFIG_ROOT
DEFAULT_CLOCK_LIMIT_MULTIPLIER,
DEFAULT_PATH_ENV,
JOJ3_CONFIG_ROOT,
)
from joj3_config_generator.utils.logger import logger from joj3_config_generator.utils.logger import logger
@ -19,14 +17,18 @@ def get_conf_stage(
task_stage: task.Stage, task_stage: task.Stage,
cached: Dict[str, None], cached: Dict[str, None],
) -> result.StageDetail: ) -> result.StageDetail:
if task_stage.groups:
groups = task_stage.groups
else:
# TODO: remove group matching
# single group is determined by adding between "[]" in the name of the task
if match := re.search(r"\[([^\[\]]+)\]", task_stage.name):
groups = [match.group(1)]
else:
groups = []
conf_stage = result.StageDetail( conf_stage = result.StageDetail(
name=task_stage.name, name=task_stage.name,
# group is determined by adding between "[]" in the name of the task groups=groups,
group=(
match.group(1)
if (match := re.search(r"\[([^\[\]]+)\]", task_stage.name or ""))
else ""
),
executor=result.Executor( executor=result.Executor(
name="sandbox", name="sandbox",
with_=get_executor_with(task_stage, cached), with_=get_executor_with(task_stage, cached),
@ -60,8 +62,9 @@ def get_parser_handler_map(
task.Parser.CPPCHECK: (fix_keyword, task_stage.cppcheck), task.Parser.CPPCHECK: (fix_keyword, task_stage.cppcheck),
task.Parser.CPPLINT: (fix_keyword, task_stage.cpplint), task.Parser.CPPLINT: (fix_keyword, task_stage.cpplint),
task.Parser.RESULT_DETAIL: (fix_result_detail, task_stage.result_detail), task.Parser.RESULT_DETAIL: (fix_result_detail, task_stage.result_detail),
task.Parser.DEBUG: (fix_debug, None),
task.Parser.DUMMY: (fix_dummy, task_stage.dummy), task.Parser.DUMMY: (fix_dummy, task_stage.dummy),
task.Parser.RESULT_STATUS: (fix_dummy, task_stage.result_status), task.Parser.RESULT_STATUS: (fix_result_status, task_stage.result_status),
task.Parser.FILE: (fix_file, task_stage.file), task.Parser.FILE: (fix_file, task_stage.file),
task.Parser.DIFF: ( task.Parser.DIFF: (
partial( partial(
@ -79,33 +82,46 @@ def get_parser_handler_map(
def get_executor_with( def get_executor_with(
task_stage: task.Stage, cached: Dict[str, None] task_stage: task.Stage, cached: Dict[str, None]
) -> result.ExecutorWith: ) -> result.ExecutorWith:
file_import = task_stage.files.import_ copy_in: dict[
copy_in_files = (file for file in file_import if file not in cached) str,
Union[result.LocalFile, result.MemoryFile, result.PreparedFile, result.Symlink],
] = {
file: result.LocalFile(src=str(JOJ3_CONFIG_ROOT / file))
for file in task_stage.files.import_
if file not in cached
}
for src, dst in task_stage.files.import_map.items():
if dst in cached:
continue
if dst in task_stage.files.import_:
logger.warning(
f"file {dst} imported multiple times. from files.import: {dst} and files.import-map: {src}"
)
copy_in[dst] = result.LocalFile(src=str(JOJ3_CONFIG_ROOT / src))
file_export = task_stage.files.export file_export = task_stage.files.export
copy_out_files = ["stdout", "stderr"] copy_out_files = ["stdout", "stderr"]
executor_with_config = result.ExecutorWith( executor_with_config = result.ExecutorWith(
default=result.Cmd( default=result.Cmd(
args=shlex.split(task_stage.command), args=shlex.split(task_stage.command),
env=[DEFAULT_PATH_ENV, *task_stage.env], env=[DEFAULT_PATH_ENV, *task_stage.env],
copy_in={ copy_in=copy_in,
file: result.LocalFile(src=str(JOJ3_CONFIG_ROOT / file))
# all copyin files store in this tools folder
# TODO: are there any corner cases?
for file in copy_in_files
},
copy_in_dir="." if task_stage.copy_in_cwd else "", copy_in_dir="." if task_stage.copy_in_cwd else "",
copy_out=copy_out_files, copy_out=copy_out_files,
copy_in_cached={file: file for file in cached}, copy_in_cached={
file: file
for file in cached
if file not in task_stage.files.no_auto_import
},
copy_out_cached=file_export, copy_out_cached=file_export,
cpu_limit=Time(task_stage.limit.cpu), cpu_limit=task_stage.limit.cpu,
clock_limit=DEFAULT_CLOCK_LIMIT_MULTIPLIER * Time(task_stage.limit.cpu), clock_limit=task_stage.limit.time,
memory_limit=Memory(task_stage.limit.mem), memory_limit=task_stage.limit.mem,
proc_limit=task_stage.limit.proc, proc_limit=task_stage.limit.proc,
stderr=result.Collector( stderr=result.Collector(
name="stderr", pipe=True, max=Memory(task_stage.limit.stderr) name="stderr", pipe=True, max=task_stage.limit.stderr
), ),
stdout=result.Collector( stdout=result.Collector(
name="stdout", pipe=True, max=Memory(task_stage.limit.stdout) name="stdout", pipe=True, max=task_stage.limit.stdout
), ),
), ),
cases=[], cases=[],
@ -124,10 +140,11 @@ def fix_keyword(
for keyword, score in zip(keyword_config.keyword, keyword_config.weight): for keyword, score in zip(keyword_config.keyword, keyword_config.weight):
score_groups.setdefault(score, []).append(keyword) score_groups.setdefault(score, []).append(keyword)
keyword_parser.with_ = result.KeywordMatchConfig( keyword_parser.with_ = result.KeywordMatchConfig(
score=keyword_config.score,
matches=[ matches=[
result.KeywordConfig(keywords=keywords, score=score) result.KeywordConfig(keywords=keywords, score=score)
for score, keywords in score_groups.items() for score, keywords in score_groups.items()
] ],
) )
@ -150,17 +167,35 @@ def fix_result_detail(
show_memory=result_detail_parser_config.mem, show_memory=result_detail_parser_config.mem,
show_error=result_detail_parser_config.error, show_error=result_detail_parser_config.error,
show_proc_peak=result_detail_parser_config.proc_peak, show_proc_peak=result_detail_parser_config.proc_peak,
files_in_code_block=result_detail_parser_config.code_block,
max_file_length=result_detail_parser_config.max_length,
) )
def fix_dummy( def fix_dummy(
dummy_parser_config: task.ParserDummy, dummy_parser: result.Parser dummy_parser_config: task.ParserDummy, dummy_parser: result.Parser
) -> None: ) -> None:
# we don't use dummy parser in real application
dummy_parser.with_ = result.DummyConfig( dummy_parser.with_ = result.DummyConfig(
score=dummy_parser_config.score, score=dummy_parser_config.score,
comment=dummy_parser_config.comment, comment=dummy_parser_config.comment,
force_quit_on_not_accepted=dummy_parser_config.force_quit, force_quit=dummy_parser_config.force_quit,
)
def fix_debug(parser_config: StrictBaseModel, parser: result.Parser) -> None:
logger.warning(
"debug parser enabled, should be disabled as soon as possible after issue is fixed"
)
def fix_result_status(
result_status_parser_config: task.ParserResultStatus,
result_status_parser: result.Parser,
) -> None:
result_status_parser.with_ = result.ResultStatusConfig(
score=result_status_parser_config.score,
comment=result_status_parser_config.comment,
force_quit_on_not_accepted=result_status_parser_config.force_quit,
) )
@ -177,18 +212,21 @@ def fix_diff(
task_path: Path, task_path: Path,
) -> None: ) -> None:
base_dir = JOJ3_CONFIG_ROOT / task_path.parent base_dir = JOJ3_CONFIG_ROOT / task_path.parent
case_base_dir = Path(task_stage.base_case_dir)
# cases not specified in the toml config (auto-detected) # cases not specified in the toml config (auto-detected)
unspecified_cases = get_unspecified_cases(task_root, task_path, task_stage.cases) unspecified_cases = get_unspecified_cases(
task_root, task_path, case_base_dir, task_stage.cases
)
# cases specified in toml config but not skipped # cases specified in toml config but not skipped
specified_cases = [ specified_cases = task_stage.cases
(case, task_stage.cases[case])
for case in task_stage.cases
if case not in task_stage.skip
]
stage_cases = [] stage_cases = []
parser_cases = [] parser_cases = []
for case_name, case in specified_cases: collected_cases = []
stdin, stdout = get_stdin_stdout(task_root, task_path, case_name, case) for case_name in specified_cases:
case = task_stage.cases[case_name]
stdin, stdout = get_stdin_stdout(
task_root, task_path, case_base_dir, case_name, case
)
if stdout is None: if stdout is None:
logger.warning( logger.warning(
f"In file {task_root / task_path}, " f"In file {task_root / task_path}, "
@ -200,7 +238,7 @@ def fix_diff(
stdin=stdin, stdin=stdin,
args=shlex.split(case.command) if case.command else None, args=shlex.split(case.command) if case.command else None,
cpu_limit=case.limit.cpu, cpu_limit=case.limit.cpu,
clock_limit=DEFAULT_CLOCK_LIMIT_MULTIPLIER * case.limit.cpu, clock_limit=case.limit.time,
memory_limit=case.limit.mem, memory_limit=case.limit.mem,
proc_limit=task_stage.limit.proc, proc_limit=task_stage.limit.proc,
) )
@ -214,51 +252,63 @@ def fix_diff(
cmd.memory_limit = None cmd.memory_limit = None
if cmd.proc_limit == executor.with_.default.proc_limit: if cmd.proc_limit == executor.with_.default.proc_limit:
cmd.proc_limit = None cmd.proc_limit = None
stage_cases.append(cmd)
def get_diff_attribute(attribute_name: str) -> Any:
if case.diff and attribute_name in case.diff.model_fields_set:
return getattr(case.diff, attribute_name)
return getattr(task_stage.diff, attribute_name)
parser_case = result.DiffCasesConfig( parser_case = result.DiffCasesConfig(
outputs=[ outputs=[
result.DiffOutputConfig( result.DiffOutputConfig(
score=case.diff.output.score, score=get_diff_attribute("score"),
file_name="stdout", filename="stdout",
answer_path=stdout, answer_path=stdout,
force_quit_on_diff=case.diff.output.force_quit, compare_space=not get_diff_attribute("ignore_spaces"),
always_hide=case.diff.output.hide, always_hide=get_diff_attribute("hide"),
compare_space=not case.diff.output.ignore_spaces, force_quit_on_diff=get_diff_attribute("force_quit"),
max_diff_length=case.diff.output.max_length, max_diff_length=get_diff_attribute("max_length"),
max_diff_lines=case.diff.output.max_lines, max_diff_lines=get_diff_attribute("max_lines"),
hide_common_prefix=case.diff.output.hide_common_prefix, hide_common_prefix=get_diff_attribute("hide_common_prefix"),
) )
] ]
) )
parser_cases.append(parser_case) collected_cases.append((stdout, cmd, parser_case))
for case_name in unspecified_cases: for case_name in unspecified_cases:
cmd = result.OptionalCmd( cmd = result.OptionalCmd(
stdin=result.LocalFile(src=str(base_dir / f"{case_name}.in")), stdin=result.LocalFile(src=str(base_dir / f"{case_name}.in")),
cpu_limit=None,
clock_limit=None,
memory_limit=None,
proc_limit=None,
) )
stage_cases.append(cmd) stdout = str(base_dir / f"{case_name}.out")
parser_case = result.DiffCasesConfig( parser_case = result.DiffCasesConfig(
outputs=[ outputs=[
result.DiffOutputConfig( result.DiffOutputConfig(
score=task_stage.diff.default_score, score=task_stage.diff.score,
file_name="stdout", filename="stdout",
answer_path=str(base_dir / f"{case_name}.out"), answer_path=stdout,
compare_space=not task_stage.diff.ignore_spaces,
always_hide=task_stage.diff.hide,
force_quit_on_diff=task_stage.diff.force_quit,
max_diff_length=task_stage.diff.max_length,
max_diff_lines=task_stage.diff.max_lines,
hide_common_prefix=task_stage.diff.hide_common_prefix,
) )
] ]
) )
parser_cases.append(parser_case) collected_cases.append((stdout, cmd, parser_case))
sorted_collected_cases = natsorted(collected_cases, key=lambda x: x[0])
stage_cases = [x[1] for x in sorted_collected_cases]
parser_cases = [x[2] for x in sorted_collected_cases]
executor.with_.cases = stage_cases executor.with_.cases = stage_cases
diff_parser.with_ = result.DiffConfig(name="diff", cases=parser_cases) diff_parser.with_ = result.DiffConfig(name="diff", cases=parser_cases)
def get_unspecified_cases( def get_unspecified_cases(
task_root: Path, task_path: Path, cases: Dict[str, task.Case] task_root: Path, task_path: Path, case_base_dir: Path, cases: Dict[str, task.Case]
) -> List[str]: ) -> Set[str]:
testcases = set() testcases = set()
for testcases_path in (task_root / task_path).parent.glob("**/*.in"): for testcases_path in ((task_root / task_path).parent / case_base_dir).glob(
"**/*.in"
):
if not testcases_path.with_suffix(".out").exists(): if not testcases_path.with_suffix(".out").exists():
logger.warning( logger.warning(
f"In file {task_root / task_path}, " f"In file {task_root / task_path}, "
@ -273,30 +323,34 @@ def get_unspecified_cases(
) )
).removesuffix(".in") ).removesuffix(".in")
) )
return sorted( return testcases.difference(
testcases.difference( casei for casei in testcases if any(casei.endswith(casej) for casej in cases)
[
casei
for casei in testcases
if any(casei.endswith(casej) for casej in cases)
]
)
) )
def get_stdin_stdout( def get_stdin_stdout(
task_root: Path, task_path: Path, case_name: str, case: task.Case task_root: Path,
task_path: Path,
case_base_dir: Path,
case_name: str,
case: task.Case,
) -> Tuple[result.Stdin, Optional[str]]: ) -> Tuple[result.Stdin, Optional[str]]:
case_stdout_name = case.out_ if case.out_ else f"{case_name}.out" base_dir = (task_root / task_path).parent / case_base_dir
stdin: result.Stdin = result.MemoryFile(content="") stdin: result.Stdin = result.MemoryFile(content="")
stdout = None stdout = None
for case_stdout_path in (task_root / task_path).parent.glob("**/*.out"): for case_stdout_path in base_dir.glob("**/*.out"):
if case_stdout_path.name != case_stdout_name: if not case.out_: # if not set, look for .out files with case name
if case_stdout_path.name != f"{case_name}.out":
continue
else: # if set, look for .out files with the same relative path
if PurePosixPath(case.out_) != PurePosixPath(case_stdout_path).relative_to(
base_dir
):
continue continue
stdout = str(JOJ3_CONFIG_ROOT / case_stdout_path.relative_to(task_root)) stdout = str(JOJ3_CONFIG_ROOT / case_stdout_path.relative_to(task_root))
case_stdin_path = case_stdout_path.with_suffix(".in") case_stdin_path = case_stdout_path.with_suffix(".in")
if case.in_: if case.in_:
case_stdin_path = Path((task_root / task_path).parent / case.in_) case_stdin_path = Path(base_dir / case.in_)
if not case_stdin_path.exists(): if not case_stdin_path.exists():
logger.warning( logger.warning(
f"In file {task_root / task_path}, " f"In file {task_root / task_path}, "

View File

@ -5,7 +5,7 @@
groups = ["default", "dev", "lint", "test"] groups = ["default", "dev", "lint", "test"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:871844bf136123c9677a605fce706c5ea8f86734f29eefcba10513c082531657" content_hash = "sha256:4e3b17128a8476ddabd32870991976c85a00b63c7a3d5e9d3f58d892375370d5"
[[metadata.targets]] [[metadata.targets]]
requires_python = ">=3.9" requires_python = ">=3.9"
@ -494,6 +494,17 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
] ]
[[package]]
name = "natsort"
version = "8.4.0"
requires_python = ">=3.7"
summary = "Simple yet flexible natural sorting in Python."
groups = ["default"]
files = [
{file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"},
{file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"},
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -510,7 +521,7 @@ name = "packaging"
version = "24.2" version = "24.2"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Core utilities for Python packages" summary = "Core utilities for Python packages"
groups = ["lint", "test"] groups = ["default", "lint", "test"]
files = [ files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
@ -835,6 +846,34 @@ files = [
{file = "runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1"}, {file = "runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1"},
] ]
[[package]]
name = "setuptools"
version = "80.9.0"
requires_python = ">=3.9"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
groups = ["default"]
files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
]
[[package]]
name = "setuptools-scm"
version = "9.2.0"
requires_python = ">=3.8"
summary = "the blessed package to manage your versions by scm tags"
groups = ["default"]
dependencies = [
"packaging>=20",
"setuptools",
"tomli>=1; python_version < \"3.11\"",
"typing-extensions; python_version < \"3.10\"",
]
files = [
{file = "setuptools_scm-9.2.0-py3-none-any.whl", hash = "sha256:c551ef54e2270727ee17067881c9687ca2aedf179fa5b8f3fab9e8d73bdc421f"},
{file = "setuptools_scm-9.2.0.tar.gz", hash = "sha256:6662c9b9497b6c9bf13bead9d7a9084756f68238302c5ed089fb4dbd29d102d7"},
]
[[package]] [[package]]
name = "shellingham" name = "shellingham"
version = "1.5.4" version = "1.5.4"

View File

@ -1,10 +1,10 @@
[build-system] [build-system]
requires = ["pdm-backend", "setuptools>=64", "setuptools-scm>=8"] requires = ["pdm-backend", "setuptools>=80", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "JOJ3-config-generator" name = "JOJ3-config-generator"
version = "0.1.0" dynamic = ["version"]
dependencies = [ dependencies = [
"pyyaml>=6.0.2", "pyyaml>=6.0.2",
"typer>=0.12.5", "typer>=0.12.5",
@ -14,6 +14,8 @@ dependencies = [
"humanfriendly>=10.0", "humanfriendly>=10.0",
"tomlkit>=0.13.2", "tomlkit>=0.13.2",
"tomli>=2.2.1", "tomli>=2.2.1",
"setuptools-scm>=9.2.0",
"natsort>=8.4.0",
] ]
requires-python = ">=3.9" requires-python = ">=3.9"
authors = [{ name = "JOJ3-dev", email = "joj3@focs.ji.sjtu.edu.cn" }] authors = [{ name = "JOJ3-dev", email = "joj3@focs.ji.sjtu.edu.cn" }]
@ -69,7 +71,6 @@ disallow_untyped_defs = true
[tool.pydantic-mypy] [tool.pydantic-mypy]
init_forbid_extra = true init_forbid_extra = true
init_typed = true init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true warn_untyped_fields = true
[tool.setuptools_scm] [tool.setuptools_scm]

View File

View File

View File

View File

View File

@ -3,14 +3,13 @@ grading-repo-name = "ece280-joj"
sandbox-token = "test" sandbox-token = "test"
# reconfigure later # reconfigure later
max-total-score = 1000 max-total-score = 1000
max-size = 50.5
health-check.max-size = "50.5m"
health-check.immutable-path = "immutable"
health-check.required-files = ["README.md", "Changelog.md"]
# for tests # for tests
[groups] [groups]
name = ["joj", "run"] name = ["joj", "run"]
max-count = [1000, 1000] max-count = [1000, 1000]
time-period-hour = [24, 24] time-period-hour = [24, 24]
[files]
required = ["README.md", "Changelog.md"]
immutable = [".gitignore", ".gitattributes",".gitea/workflows/push.yaml", ".gitea/workflows/release.yaml"]

View File

@ -1,18 +1,15 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 10245871,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "test", "sandboxToken": "test",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "Health Check", "name": "Health Check",
"group": "", "groups": [],
"executor": { "executor": {
"name": "local", "name": "local",
"with": { "with": {
@ -64,8 +61,8 @@
"-repoSize=50.5", "-repoSize=50.5",
"-meta=README.md", "-meta=README.md",
"-meta=Changelog.md", "-meta=Changelog.md",
"-checkFileSumList=a5b63323a692d3d8b952442969649b4f823d58dae26429494f613df160710dfc,b1bbad25b830db0a77b15a033f9ca1b7ab44c1d2d05056412bd3e4421645f0bf,2ba059f3977e2e3dee6cacbfbf0ba2578baa1b8e04b4977aec400868b6e49856,3db23f7fb2ca9814617e767ddc41b77073180b3b0b73e87b5f2a6d3129f88f3a", "-checkFileSumList=b1bbad25b830db0a77b15a033f9ca1b7ab44c1d2d05056412bd3e4421645f0bf,2ba059f3977e2e3dee6cacbfbf0ba2578baa1b8e04b4977aec400868b6e49856,3db23f7fb2ca9814617e767ddc41b77073180b3b0b73e87b5f2a6d3129f88f3a,a5b63323a692d3d8b952442969649b4f823d58dae26429494f613df160710dfc",
"-checkFileNameList=.gitignore,.gitattributes,.gitea/workflows/push.yaml,.gitea/workflows/release.yaml" "-checkFileNameList=.gitattributes,.gitea/workflows/push.yaml,.gitea/workflows/release.yaml,.gitignore"
] ]
}, },
{ {
@ -75,10 +72,19 @@
"/home/tt/.config/teapot/teapot.env", "/home/tt/.config/teapot/teapot.env",
"--grading-repo-name", "--grading-repo-name",
"ece280-joj", "ece280-joj",
"--scoreboard-filename",
"scoreboard-hw7.csv",
"--group-config", "--group-config",
"Manuel=500:24,Boming=501:48,Nuvole=502:72" "Manuel=500:24,Boming=501:48,Nuvole=502:72",
"--begin-time",
"2024-12-29T23:59:59",
"--end-time",
"2024-12-30T23:59:59",
"--penalty-config",
"24.0=0.5,48.0=0.25,72.0=0.1"
], ],
"env": [ "env": [
"REPOS_DIR=/home/tt/.cache",
"LOG_FILE_PATH=/home/tt/.cache/joint-teapot-debug.log" "LOG_FILE_PATH=/home/tt/.cache/joint-teapot-debug.log"
] ]
} }
@ -100,7 +106,7 @@
}, },
{ {
"name": "Compilation", "name": "Compilation",
"group": "", "groups": [],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -136,6 +142,9 @@
"copyIn": { "copyIn": {
"tools/compile": { "tools/compile": {
"src": "/home/tt/.config/joj/tools/compile" "src": "/home/tt/.config/joj/tools/compile"
},
"h7/Makefile": {
"src": "/home/tt/.config/joj/tools/Makefile"
} }
}, },
"copyInCached": {}, "copyInCached": {},
@ -193,7 +202,9 @@
}, },
{ {
"name": "[cq] Filelength", "name": "[cq] Filelength",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -260,6 +271,7 @@
{ {
"name": "keyword", "name": "keyword",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -299,7 +311,9 @@
}, },
{ {
"name": "[cq] Clang-tidy", "name": "[cq] Clang-tidy",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -368,6 +382,7 @@
{ {
"name": "clangtidy", "name": "clangtidy",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -433,7 +448,9 @@
}, },
{ {
"name": "[cq] Cppcheck", "name": "[cq] Cppcheck",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -498,12 +515,14 @@
{ {
"name": "keyword", "name": "keyword",
"with": { "with": {
"score": 0,
"matches": [] "matches": []
} }
}, },
{ {
"name": "cppcheck", "name": "cppcheck",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -526,6 +545,7 @@
{ {
"name": "clangtidy", "name": "clangtidy",
"with": { "with": {
"score": 0,
"matches": [] "matches": []
} }
}, },
@ -551,6 +571,7 @@
{ {
"name": "cpplint", "name": "cpplint",
"with": { "with": {
"score": 0,
"matches": [] "matches": []
} }
}, },
@ -559,7 +580,7 @@
"with": { "with": {
"score": 0, "score": 0,
"comment": "", "comment": "",
"forceQuitOnNotAccepted": false "forceQuitOnNotAccepted": true
} }
}, },
{ {
@ -573,7 +594,7 @@
"with": { "with": {
"score": 0, "score": 0,
"comment": "", "comment": "",
"forceQuitOnNotAccepted": false "forceQuit": false
} }
}, },
{ {
@ -587,7 +608,9 @@
}, },
{ {
"name": "[cq] Cpplint", "name": "[cq] Cpplint",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -651,6 +674,7 @@
{ {
"name": "cpplint", "name": "cpplint",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -696,7 +720,9 @@
}, },
{ {
"name": "[joj] ex2-asan", "name": "[joj] ex2-asan",
"group": "joj", "groups": [
"joj"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -722,7 +748,7 @@
"pipe": true "pipe": true
}, },
"cpuLimit": 1000000000, "cpuLimit": 1000000000,
"clockLimit": 2000000000, "clockLimit": 100000000000,
"memoryLimit": 134217728, "memoryLimit": 134217728,
"stackLimit": 0, "stackLimit": 0,
"procLimit": 50, "procLimit": 50,
@ -749,7 +775,27 @@
"dataSegmentLimit": false, "dataSegmentLimit": false,
"addressSpaceLimit": false "addressSpaceLimit": false
}, },
"cases": [] "cases": [
{
"stdin": {
"content": ""
},
"cpuLimit": 500000000,
"clockLimit": 1000000000,
"memoryLimit": 5242880
},
{
"stdin": {
"content": ""
},
"memoryLimit": 5242880
},
{
"stdin": {
"content": ""
}
}
]
} }
}, },
"parsers": [ "parsers": [
@ -757,7 +803,53 @@
"name": "diff", "name": "diff",
"with": { "with": {
"name": "diff", "name": "diff",
"cases": [] "cases": [
{
"outputs": [
{
"score": 10,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/basic/cases/case0.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 5,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/basic/cases/case1.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 10,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/basic/cases/case2.out",
"compareSpace": false,
"alwaysHide": true,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
}
]
} }
}, },
{ {
@ -782,11 +874,10 @@
] ]
} }
], ],
"preStages": [],
"postStages": [ "postStages": [
{ {
"name": "teapot", "name": "teapot",
"group": "", "groups": [],
"executor": { "executor": {
"name": "local", "name": "local",
"with": { "with": {
@ -798,9 +889,20 @@
"--grading-repo-name", "--grading-repo-name",
"ece280-joj", "ece280-joj",
"--max-total-score", "--max-total-score",
"10245871" "10245871",
"--issue-label-name",
"Kind/Testing",
"--issue-label-color",
"#795548",
"--scoreboard-filename",
"scoreboard-hw7.csv",
"--end-time",
"2024-12-30T23:59:59",
"--penalty-config",
"24.0=0.5,48.0=0.25,72.0=0.1"
], ],
"env": [ "env": [
"REPOS_DIR=/home/tt/.cache",
"LOG_FILE_PATH=/home/tt/.cache/joint-teapot-debug.log" "LOG_FILE_PATH=/home/tt/.cache/joint-teapot-debug.log"
], ],
"stdin": { "stdin": {
@ -845,7 +947,17 @@
{ {
"name": "log", "name": "log",
"with": { "with": {
"msg": "joj3 summary" "filename": "stdout",
"msg": "joj3 summary",
"level": -4
}
},
{
"name": "log",
"with": {
"filename": "stderr",
"msg": "joint-teapot stderr",
"level": 0
} }
}, },
{ {
@ -856,4 +968,3 @@
} }
] ]
} }
}

View File

@ -1,9 +1,14 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
max-total-score = 10245871 max-total-score = 10245871
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
scoreboard = "auto"
[penalties]
hours = [ 24, 48, 72 ]
factors = [ 0.5, 0.25, 0.1 ]
[groups] [groups]
name = ["Manuel", "Boming", "Nuvole"] name = ["Manuel", "Boming", "Nuvole"]
@ -15,8 +20,8 @@ name = "Compilation"
env = [ "CC=clang", "CXX=clang++" ] env = [ "CC=clang", "CXX=clang++" ]
command = "./tools/compile" # eg. script running cmake commands command = "./tools/compile" # eg. script running cmake commands
files.import = [ "tools/compile" ] files.import = [ "tools/compile" ]
files.import-map = { "tools/Makefile" = "h7/Makefile" }
files.export = [ "h7/build/ex2", "h7/build/ex2-asan", "h7/build/ex2-ubsan", "h7/build/ex2-msan", "h7/build/compile_commands.json" ] files.export = [ "h7/build/ex2", "h7/build/ex2-asan", "h7/build/ex2-ubsan", "h7/build/ex2-msan", "h7/build/compile_commands.json" ]
score = 1
# compile parsers # compile parsers
parsers = [ "result-detail", "result-status" ] parsers = [ "result-detail", "result-status" ]
@ -86,24 +91,29 @@ command="./h7/build/ex2-asan -a"
copy-in-cwd = false copy-in-cwd = false
files.import = [ "h7/build/ex2-asan" ] files.import = [ "h7/build/ex2-asan" ]
limit.mem = "128m" limit.mem = "128m"
limit.time = "100s"
diff.score = 10
parsers = [ "diff", "result-detail" ] parsers = [ "diff", "result-detail" ]
result-detail.exit-status = true result-detail.exit-status = true
result-detail.stderr = true result-detail.stderr = true
# will be removed as long as the name is fixed # will be removed as long as the name is fixed
case0.diff.output.score = 5
case0.limit.cpu = "0.5s" case0.limit.cpu = "0.5s"
case0.limit.time = "1s"
case0.limit.mem = "5m" case0.limit.mem = "5m"
case0.diff.output.ignore-spaces = true case0.diff.ignore-spaces = true
#case0.limit.stdout = 8 #case0.limit.stdout = 8
#case0.command = "./h7/build/ex2" #case0.command = "./h7/build/ex2"
case0.in = "case0.in" case0.in = "case0.in"
case1.diff.output.score = 5 case1.diff.score = 5
case1.limit.cpu = "1s" case1.limit.cpu = "1s"
case1.limit.mem = "5m" case1.limit.mem = "5m"
case1.diff.output.ignore-spaces = true case1.diff.ignore-spaces = true
#case1.limit.stdout = 8 #case1.limit.stdout = 8
#case1.command = "./h7/build/ex2" #case1.command = "./h7/build/ex2"
case1.in = "case1.in" case1.in = "case1.in"
case2.diff.hide = true

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] Clang-tidy", "name": "[cq] Clang-tidy",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -78,6 +77,7 @@
{ {
"name": "clangtidy", "name": "clangtidy",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -142,7 +142,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[cq] Clang-tidy" name = "[cq] Clang-tidy"

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] Cppcheck", "name": "[cq] Cppcheck",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -71,6 +70,7 @@
{ {
"name": "cppcheck", "name": "cppcheck",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -112,7 +112,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[cq] Cppcheck" name = "[cq] Cppcheck"

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] Cpplint", "name": "[cq] Cpplint",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -70,6 +69,7 @@
{ {
"name": "cpplint", "name": "cpplint",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -114,7 +114,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[cq] Cpplint" name = "[cq] Cpplint"

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[joj] ex2-asan", "name": "[joj] ex2-asan",
"group": "joj", "groups": [
"joj"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -86,6 +85,11 @@
"clockLimit": 4000000000, "clockLimit": 4000000000,
"memoryLimit": 4194304 "memoryLimit": 4194304
}, },
{
"stdin": {
"src": "/home/tt/.config/joj/diff/case2.in"
}
},
{ {
"stdin": { "stdin": {
"src": "/home/tt/.config/joj/diff/case9.in" "src": "/home/tt/.config/joj/diff/case9.in"
@ -93,12 +97,7 @@
}, },
{ {
"stdin": { "stdin": {
"src": "/home/tt/.config/joj/diff/task1/subtask1/case11.in" "src": "/home/tt/.config/joj/diff/task1/case4.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task1/subtask1/case10.in"
} }
}, },
{ {
@ -108,12 +107,12 @@
}, },
{ {
"stdin": { "stdin": {
"src": "/home/tt/.config/joj/diff/case2.in" "src": "/home/tt/.config/joj/diff/task1/subtask1/case10.in"
} }
}, },
{ {
"stdin": { "stdin": {
"src": "/home/tt/.config/joj/diff/task1/case4.in" "src": "/home/tt/.config/joj/diff/task1/subtask1/case11.in"
} }
}, },
{ {
@ -144,7 +143,7 @@
"outputs": [ "outputs": [
{ {
"score": 5, "score": 5,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/case0.out", "answerPath": "/home/tt/.config/joj/diff/case0.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -159,7 +158,7 @@
"outputs": [ "outputs": [
{ {
"score": 123214122421, "score": 123214122421,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/case1.out", "answerPath": "/home/tt/.config/joj/diff/case1.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -170,71 +169,11 @@
} }
] ]
}, },
{
"outputs": [
{
"score": 1232131,
"fileName": "stdout",
"answerPath": "/home/tt/.config/joj/diff/case9.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 92321,
"fileName": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case11.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 823131,
"fileName": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case10.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 2590,
"fileName": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case5.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{ {
"outputs": [ "outputs": [
{ {
"score": 100, "score": 100,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/case2.out", "answerPath": "/home/tt/.config/joj/diff/case2.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -248,9 +187,9 @@
{ {
"outputs": [ "outputs": [
{ {
"score": 100, "score": 1232131,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case4.out", "answerPath": "/home/tt/.config/joj/diff/case9.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
"forceQuitOnDiff": false, "forceQuitOnDiff": false,
@ -264,7 +203,67 @@
"outputs": [ "outputs": [
{ {
"score": 100, "score": 100,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case4.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 2590,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case5.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 823131,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case10.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 92321,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case11.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case6.out", "answerPath": "/home/tt/.config/joj/diff/task2/case6.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -279,7 +278,7 @@
"outputs": [ "outputs": [
{ {
"score": 100, "score": 100,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case7.out", "answerPath": "/home/tt/.config/joj/diff/task2/case7.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -294,7 +293,7 @@
"outputs": [ "outputs": [
{ {
"score": 100, "score": 100,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case8.out", "answerPath": "/home/tt/.config/joj/diff/task2/case8.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -328,9 +327,365 @@
} }
} }
] ]
},
{
"name": "[joj] ex2-asan base dir task1",
"groups": [
"joj"
],
"executor": {
"name": "sandbox",
"with": {
"default": {
"args": [
"./h7/build/ex2-asan",
"-a"
],
"env": [
"PATH=/usr/bin:/bin:/usr/local/bin"
],
"stdin": {
"content": ""
},
"stdout": {
"name": "stdout",
"max": 10485760,
"pipe": true
},
"stderr": {
"name": "stderr",
"max": 10485760,
"pipe": true
},
"cpuLimit": 3000000000,
"clockLimit": 6000000000,
"memoryLimit": 10485760,
"stackLimit": 0,
"procLimit": 50,
"cpuRateLimit": 0,
"cpuSetLimit": "",
"copyIn": {
"h7/build/ex2-asan": {
"src": "/home/tt/.config/joj/h7/build/ex2-asan"
}
},
"copyInCached": {},
"copyInDir": ".",
"copyOut": [
"stdout",
"stderr"
],
"copyOutCached": [],
"copyOutMax": 0,
"copyOutDir": "",
"tty": false,
"strictMemoryLimit": false,
"dataSegmentLimit": false,
"addressSpaceLimit": false
},
"cases": [
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task1/case4.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task1/case5.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task1/case9.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task1/subtask1/case10.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task1/subtask1/case11.in"
}
}
]
}
},
"parsers": [
{
"name": "diff",
"with": {
"name": "diff",
"cases": [
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case4.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 2590,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case5.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 99999,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case9.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case10.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case11.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
}
]
}
},
{
"name": "result-detail",
"with": {
"score": 0,
"comment": "",
"showExxecutorStatus": true,
"showExitStatus": true,
"showError": false,
"showTime": true,
"showMemory": true,
"showRuntime": true,
"showProcPeak": false,
"showFiles": [
"stderr"
],
"filesInCodeBlock": true,
"maxFileLength": 2048
}
}
]
},
{
"name": "[joj] ex2-asan base dir task2",
"groups": [
"joj"
],
"executor": {
"name": "sandbox",
"with": {
"default": {
"args": [
"./h7/build/ex2-asan",
"-a"
],
"env": [
"PATH=/usr/bin:/bin:/usr/local/bin"
],
"stdin": {
"content": ""
},
"stdout": {
"name": "stdout",
"max": 10485760,
"pipe": true
},
"stderr": {
"name": "stderr",
"max": 10485760,
"pipe": true
},
"cpuLimit": 3000000000,
"clockLimit": 6000000000,
"memoryLimit": 10485760,
"stackLimit": 0,
"procLimit": 50,
"cpuRateLimit": 0,
"cpuSetLimit": "",
"copyIn": {
"h7/build/ex2-asan": {
"src": "/home/tt/.config/joj/h7/build/ex2-asan"
}
},
"copyInCached": {},
"copyInDir": ".",
"copyOut": [
"stdout",
"stderr"
],
"copyOutCached": [],
"copyOutMax": 0,
"copyOutDir": "",
"tty": false,
"strictMemoryLimit": false,
"dataSegmentLimit": false,
"addressSpaceLimit": false
},
"cases": [
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task2/case6.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task2/case7.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task2/case8.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/diff/task2/case9.in"
}
}
]
}
},
"parsers": [
{
"name": "diff",
"with": {
"name": "diff",
"cases": [
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case6.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case7.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 100,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case8.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 99999,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task2/case9.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
}
]
}
},
{
"name": "result-detail",
"with": {
"score": 0,
"comment": "",
"showExxecutorStatus": true,
"showExitStatus": true,
"showError": false,
"showTime": true,
"showMemory": true,
"showRuntime": true,
"showProcPeak": false,
"showFiles": [
"stderr"
],
"filesInCodeBlock": true,
"maxFileLength": 2048
}
}
]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[joj] ex2-asan" name = "[joj] ex2-asan"
@ -17,26 +17,63 @@ parsers = [ "diff", "result-detail" ]
result-detail.exit-status = true result-detail.exit-status = true
result-detail.stderr = true result-detail.stderr = true
diff.default-score = 100 diff.score = 100
case0.diff.output.score = 5 case0.diff.score = 5
case0.limit.cpu = "1s" case0.limit.cpu = "1s"
case0.limit.mem = "2m" case0.limit.mem = "2m"
case0.diff.output.ignore-spaces = true case0.diff.ignore-spaces = true
case0.command = "./h7/build/ex2" case0.command = "./h7/build/ex2"
case0.in = "case0.in" case0.in = "case0.in"
case1.diff.output.score = 123214122421 case1.diff.score = 123214122421
case1.limit.cpu = "2s" case1.limit.cpu = "2s"
case1.limit.mem = "4m" case1.limit.mem = "4m"
case1.diff.output.ignore-spaces = true case1.diff.ignore-spaces = true
case1.command = "./h7/build/ex2" case1.command = "./h7/build/ex2"
case9.diff.output.score = 1232131 case9.diff.score = 1232131
case9.limit.mem = "10m" case9.limit.mem = "10m"
case11.diff.output.score = 92321 case11.diff.score = 92321
case10.diff.output.score = 823131 case10.diff.score = 823131
case5.diff.output.score = 2590 case5.diff.score = 2590
[[stages]]
name = "[joj] ex2-asan base dir task1"
command = "./h7/build/ex2-asan -a"
base-case-dir = "task1"
files.import = ["h7/build/ex2-asan"]
limit.cpu = "3s"
limit.mem = "10m"
limit.stdout = "10m"
limit.stderr = "10m"
parsers = ["diff", "result-detail"]
result-detail.exit-status = true
result-detail.stderr = true
diff.score = 100
case5.diff.score = 2590
case9.diff.score = 99999
[[stages]]
name = "[joj] ex2-asan base dir task2"
command = "./h7/build/ex2-asan -a"
base-case-dir = "task2"
files.import = ["h7/build/ex2-asan"]
limit.cpu = "3s"
limit.mem = "10m"
limit.stdout = "10m"
limit.stderr = "10m"
parsers = ["diff", "result-detail"]
result-detail.exit-status = true
result-detail.stderr = true
diff.score = 100
case9.diff.score = 99999

View File

View File

View File

View File

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] elf", "name": "[cq] elf",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -74,6 +73,7 @@
{ {
"name": "elf", "name": "elf",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -122,7 +122,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[cq] elf" name = "[cq] elf"

View File

View File

@ -0,0 +1,187 @@
{
"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",
"-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": {}
}
]
}
]
}

View File

@ -0,0 +1,2 @@
name = "health check"
max-total-score = 0

View File

View File

@ -0,0 +1,144 @@
{
"name": "Config generation failure",
"logPath": "/home/tt/.cache/joj3/joj3.log",
"actorCsvPath": "",
"sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "",
"outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [
{
"name": "Error Message",
"groups": [],
"executor": {
"name": "dummy",
"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": 1000000000,
"clockLimit": 2000000000,
"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": "dummy",
"with": {
"score": 0,
"comment": "Config generation failure. Contact teaching team to check course-joj repo actions for details.",
"forceQuit": true
}
}
]
}
],
"postStages": [
{
"name": "teapot",
"groups": [],
"executor": {
"name": "local",
"with": {
"default": {
"args": [
"/usr/local/bin/joint-teapot",
"joj3-all-env",
"/home/tt/.config/teapot/teapot.env",
"--skip-scoreboard",
"--skip-failed-table"
],
"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": "stderr",
"msg": "joint-teapot stderr",
"level": 0
}
},
{
"name": "debug",
"with": {}
}
]
}
]
}

View File

@ -0,0 +1,4 @@
name = "extra-field"
extra-field = "extra-field value"
extra-dict.extra-field = "extra-dict.extra-field value"
limit.extra-field = "limit.extra-field value"

View File

View File

View File

View File

View File

View File

View File

@ -0,0 +1,33 @@
*.avi filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.djvu filter=lfs diff=lfs merge=lfs -text
*.doc filter=lfs diff=lfs merge=lfs -text
*.docx filter=lfs diff=lfs merge=lfs -text
*.epub filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.ipynb filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.JPEG filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.JPG filter=lfs diff=lfs merge=lfs -text
*.mkv filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.ods filter=lfs diff=lfs merge=lfs -text
*.odt filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.PDF filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.PNG filter=lfs diff=lfs merge=lfs -text
*.ppt filter=lfs diff=lfs merge=lfs -text
*.pptx filter=lfs diff=lfs merge=lfs -text
*.ps filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
*.xls filter=lfs diff=lfs merge=lfs -text
*.xlsx filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text

View File

@ -0,0 +1,19 @@
name: Run JOJ3 on Push
on: [push]
jobs:
run:
container:
runs-on: focs-latest-slim
volumes:
- /home/tt/.config:/home/tt/.config
- /home/tt/.cache:/home/tt/.cache
- /home/tt/.ssh:/home/tt/.ssh
steps:
- name: Check out repository code
uses: actions/checkout@focs
with:
fetch-depth: 0
- name: Run joj3
run: |
sudo -E -u tt joj3 -conf-root /home/tt/.config/joj/tests/homework

View File

@ -0,0 +1,21 @@
name: Run JOJ3 on Release
on:
release:
types: [published]
jobs:
run:
runs-on: focs-latest-slim
container:
volumes:
- /home/tt/.config:/home/tt/.config
- /home/tt/.cache:/home/tt/.cache
- /home/tt/.ssh:/home/tt/.ssh
steps:
- name: Check out repository code
uses: actions/checkout@focs
with:
fetch-depth: 0
- name: Run joj3
run: |
sudo -E -u tt joj3 -conf-root "/home/tt/.config/joj/tests/homework" -conf-name "conf-release.json" -tag "${{ gitea.ref_name }}"

23
tests/convert/full/immutable/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
################################
## White list based gitignore ##
################################
# forbidden
*
.*
# allowed
!.gitignore
!.gitattributes
!.gitea/
!.gitea/issue_template/
!.gitea/workflows/
!*.yaml
!Makefile
!CMakeLists.txt
!h[0-8]/
!*.m
!*.c
!*.cpp
!*.h
!*.md

View File

View File

View File

@ -0,0 +1,24 @@
sandbox-token = "" # sandbox token
health-check.score = 0 # score for health check stage
health-check.max-size = "10m" # max size of the repository
health-check.whitelisted-chars = "あいうえお" # allowed non-ASCII characters in healthcheck stage
health-check.immutable-path = "immutable" # path for immutable files, relative to the path of repo.toml
health-check.required-files = ["README.md", "Changelog.md"] # required files name, case insensitive
issue.label.name = "Kind/Testing" # label for issues
issue.label.color = "#795548" # color for the label
issue.label.exclusive = false # whether the label is exclusive
issue.show-submitter = true # whether to show submitter in the issue title
# fields below can be overridden by task.toml
max-total-score = 100 # maximum total score for the task
# submission count limit groups
# explanation of the following config:
# in last 1 hour, total submission <= 50 times
# in last 24 hours, submission includes group "joj" <= 1000 times
# in last 2 hours, submission includes group "run" <= 100 times
groups.name = ["", "joj", "run"] # names of the groups
groups.max-count = [50, 1000, 100] # maximum submission count for each group
groups.time-period-hour = [1, 24, 2] # time period in hour for each group

View File

@ -0,0 +1,525 @@
{
"name": "hw7 ex3",
"logPath": "/home/tt/.cache/joj3/hw7/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",
"-meta=README.md",
"-meta=Changelog.md",
"-whitelistedChars=あ,い,う,え,お",
"-checkFileSumList=b1bbad25b830db0a77b15a033f9ca1b7ab44c1d2d05056412bd3e4421645f0bf,2ba059f3977e2e3dee6cacbfbf0ba2578baa1b8e04b4977aec400868b6e49856,3db23f7fb2ca9814617e767ddc41b77073180b3b0b73e87b5f2a6d3129f88f3a,a5b63323a692d3d8b952442969649b4f823d58dae26429494f613df160710dfc",
"-checkFileNameList=.gitattributes,.gitea/workflows/push.yaml,.gitea/workflows/release.yaml,.gitignore"
]
},
{
"args": [
"/usr/local/bin/joint-teapot",
"joj3-check-env",
"/home/tt/.config/teapot/teapot.env",
"--grading-repo-name",
"JOJ3-config-generator",
"--scoreboard-filename",
"scoreboard-hw7.csv",
"--group-config",
"joj=1000:24,run=100:1",
"--begin-time",
"2024-12-29T23:59:59",
"--end-time",
"2024-12-30T23:59:59",
"--penalty-config",
"24.0=0.75,48.0=0.5,72.0=0.25"
],
"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": {}
}
]
},
{
"name": "Generate yes.txt [no]",
"groups": [
"run",
"no"
],
"executor": {
"name": "sandbox",
"with": {
"default": {
"args": [
"sh",
"-c",
"yes | head -n 10 > yes.txt"
],
"env": [
"PATH=/usr/bin:/bin:/usr/local/bin",
"THE_ANSWER=42"
],
"stdin": {
"content": ""
},
"stdout": {
"name": "stdout",
"max": 33554432,
"pipe": true
},
"stderr": {
"name": "stderr",
"max": 33554432,
"pipe": true
},
"cpuLimit": 1000000000,
"clockLimit": 2000000000,
"memoryLimit": 268435456,
"stackLimit": 0,
"procLimit": 50,
"cpuRateLimit": 0,
"cpuSetLimit": "",
"copyIn": {
"tools/filelength": {
"src": "/home/tt/.config/joj/tools/filelength"
},
"h7/Makefile": {
"src": "/home/tt/.config/joj/tools/Makefile"
}
},
"copyInCached": {},
"copyInDir": ".",
"copyOut": [
"stdout",
"stderr"
],
"copyOutCached": [
"yes.txt"
],
"copyOutMax": 0,
"copyOutDir": "",
"tty": false,
"strictMemoryLimit": false,
"dataSegmentLimit": false,
"addressSpaceLimit": false
},
"cases": [
{
"stdin": {
"src": "/home/tt/.config/joj/full/cases/case0.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/full/cases/case1.in"
}
},
{
"stdin": {
"src": "/home/tt/.config/joj/full/cases/case2.in"
},
"cpuLimit": 2000000000,
"memoryLimit": 536870912
},
{
"stdin": {
"src": "/home/tt/.config/joj/full/other/cases/case3.in"
}
}
]
}
},
"parsers": [
{
"name": "result-status",
"with": {
"score": 0,
"comment": "Congrats! There is a yes.txt file generated!",
"forceQuitOnNotAccepted": true
}
},
{
"name": "result-detail",
"with": {
"score": 0,
"comment": "",
"showExxecutorStatus": true,
"showExitStatus": true,
"showError": false,
"showTime": true,
"showMemory": true,
"showRuntime": true,
"showProcPeak": false,
"showFiles": [],
"filesInCodeBlock": true,
"maxFileLength": 2048
}
},
{
"name": "keyword",
"with": {
"score": 0,
"matches": [
{
"keywords": [
"aaa"
],
"score": 20
},
{
"keywords": [
"bbb"
],
"score": 10
},
{
"keywords": [
"ccc"
],
"score": 5
}
]
}
},
{
"name": "clangtidy",
"with": {
"score": 0,
"matches": [
{
"keywords": [
"clang-diagnostic",
"clang-analyzer",
"misc",
"performance",
"portability"
],
"score": 5
}
]
}
},
{
"name": "cppcheck",
"with": {
"score": 0,
"matches": [
{
"keywords": [
"error"
],
"score": 15
},
{
"keywords": [
"warning",
"portability",
"performance",
"style"
],
"score": 5
}
]
}
},
{
"name": "cpplint",
"with": {
"score": 0,
"matches": [
{
"keywords": [
"runtime"
],
"score": 5
},
{
"keywords": [
"readability"
],
"score": 20
},
{
"keywords": [
"build"
],
"score": 10
}
]
}
},
{
"name": "elf",
"with": {
"score": 0,
"matches": [
{
"keywords": [
"Parentheses"
],
"score": 100
},
{
"keywords": [
"Length"
],
"score": 300
},
{
"keywords": [
"Arity"
],
"score": 50
},
{
"keywords": [
"Repetitive"
],
"score": 80
}
]
}
},
{
"name": "diff",
"with": {
"name": "diff",
"cases": [
{
"outputs": [
{
"score": 5,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/full/cases/case0.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 5,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/full/cases/case1.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 5,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/full/cases/case2.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
},
{
"outputs": [
{
"score": 5,
"filename": "stdout",
"answerPath": "/home/tt/.config/joj/full/other/cases/case3.out",
"compareSpace": false,
"alwaysHide": false,
"forceQuitOnDiff": false,
"maxDiffLength": 2048,
"maxDiffLines": 50,
"hideCommonPrefix": false
}
]
}
]
}
},
{
"name": "dummy",
"with": {
"score": 0,
"comment": "",
"forceQuit": false
}
},
{
"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",
"100",
"--issue-label-name",
"Kind/Testing",
"--issue-label-color",
"#795548",
"--scoreboard-filename",
"scoreboard-hw7.csv",
"--scoreboard-column-by-ref",
"--end-time",
"2024-12-30T23:59:59",
"--penalty-config",
"24.0=0.75,48.0=0.5,72.0=0.25"
],
"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": {}
}
]
}
]
}

View File

@ -0,0 +1,228 @@
name = "hw7 ex3" # task name, will be shown in the issue title
# scoreboard file name in grading repo, "auto" for automatic generation, default: scoreboard.csv
scoreboard = "auto"
# scoreboard = "scoreboard-42.csv" # use this if you want to specify a custom scoreboard
# whether to use gitea ref as scoreboard column, instead of the task name, default: false
scoreboard-column-by-ref = true
# skip creating issue after job done, default: false
issue.skip = false
# skip updating scoreboard in grading branch in course-joj repo after job done, default: false
grading.skip-scoreboard = false
# skip updating failed table in grading branch in course-joj repo after job done, default: false
grading.skip-failed-table = false
# task triggered not in this time period will not pass the health check
time.begin = 2024-12-29 23:59:59 # begin time, default: no start time, do not check
time.end = 2024-12-30 23:59:59 # end time, default: no end time, do not check
# explanation of the following config:
# if the submission is within 0-24 hours late from time.end,
# the final score will be multiplied by 0.75
# if the submission is within 24-48 hours, multiplied by 0.5
# if the submission is within 48-72 hours, multiplied by 0.25
penalties.hours = [24, 48, 72] # penalty hours for late submissions
penalties.factors = [0.75, 0.5, 0.25] # penalty factors for late submissions
# check repo.toml, fields below can override repo.toml
max-total-score = 100
groups.name = ["joj", "run"]
groups.max-count = [1000, 100]
groups.time-period-hour = [24, 1]
# default: false
# when set to true, even if the submitter is different,
# it will still count toward the submission count
# valid for task.toml only
groups.ignore-submitter = false
# list of stages
[[stages]]
# stage name, content in the `[]` set the group
# conventional commit message needs to contain the group name to run it
# group can be omitted so that every commit will run this stage
# e.g. commit msg "test(hw7): run yes" will not run this stage
# commit msg "test(hw7): run yes [no]" will run this stage
name = "Generate yes.txt [no]"
# you can also use this fields to override the above groups
groups = ["run", "no"]
# default dir to automatically searching for cases
base-case-dir = "."
# ===================================================
# ========== executor related config start ==========
# ===================================================
# executor runs the command in a limited sandbox, each stage uses a unique new sandbox
# by default the sandbox does not share files and env vars with the host
# so we need to set env vars and import files to it, and export files for later stages
# limits can be applied on time, memory, file size, process count
# environment variables, will be set in the sandbox
# by default, "PATH=/usr/bin:/bin:/usr/local/bin" will be inserted in the front
env = ["THE_ANSWER=42"]
command = "sh -c 'yes | head -n 10 > yes.txt'" # command to run in the sandbox, use `sh -c` to run shell commands
# files to import into the sandbox, relative to `/home/tt/.config/joj/`
# e.g. this will copy `/home/tt/.config/joj/tools/filelength` in host
# to `/w/tools/filelength` in the sandbox as work dir in sandbox is `/w`
files.import = [ "tools/filelength" ]
# files to import into the sandbox
# key is the path in the host, value is the path in the sandbox
# e.g. this will copy `/home/tt/.config/joj/tools/Makefile` in host
# to `/w/h7/Makefile` in the sandbox
files.import-map = { "tools/Makefile" = "h7/Makefile" }
# files to export from the sandbox, relative to `/w`, will be imported to later
# stages automatically
files.export = [ "yes.txt" ]
# files that should not be imported from the `export` field in previous stages
files.no-auto-import = [ "yes.txt" ]
# whether to copy all files from the current working directory from host (i.e. the whole repo)
# you can set it as false if you are in the run stage of a compiled language, as the binary is all you need
copy-in-cwd = true # default: true
# normally, you DO NOT need to change these default limits
limit.cpu = "1s" # CPU time limit, default: "1s"
limit.mem = "256m" # memory limit, default: "256m"
limit.time = "2s" # wall-clock time limit, if not set, will be 2x CPU time
limit.stdout = "32m" # stdout size limit, default: "32m"
limit.stderr = "32m" # stderr size limit, default: "32m"
limit.proc = 50 # process limit, default: 50
# =================================================
# ========== executor related config end ==========
# =================================================
# =================================================
# ========== parser related config start ==========
# =================================================
# parser parses the output of the executor and generates comments
# parsers to use for this stage
# parsers will be run in the order they are listed,
# which defines the order of the output comment
# all possible parsers are listed here
# usually, only one of the match keywords style parsers should be used in one stage
parsers = [
"result-status", # check if result status is Accepted
"result-detail", # show result details (CPU time, memory, etc.)
# ========== match keywords style parsers start ========
"keyword", # match keywords in the output and score them
"clangtidy", # parse clang-tidy output, and match keywords
"cppcheck", # parse cppcheck output, and match keywords
"cpplint", # parse cpplint output, and match keywords
"elf", # parse elf (static analyzer for elm) output, and match keywords
# ========== match keywords style parsers end ==========
"diff", # diff the output with the expected output
# ======================================================
"dummy", # dummy parser, used to show a comment
"debug", # debug parser, will log executor output files
]
result-status.score = 0 # score added if result status is Accepted, default: 0
result-status.comment = "Congrats! There is a yes.txt file generated!" # comment to show if result status is Accepted, default: ""
result-status.force-quit = true # whether to force quit the stage if result status is not Accepted, default: true
result-detail.cpu-time = true # show CPU time, default: true
result-detail.mem = true # show memory usage, default: true
result-detail.time = true # show wall-clock time, default: true
result-detail.stdout = false # show stdout content, default: false
result-detail.stderr = false # show stderr content, default: false
result-detail.exit-status = true # show exit status, default: true
result-detail.proc-peak = false # show peak process count, default: false
result-detail.error = false # show error message, default: false
result-detail.code-block = true # show result in a code block, default: true
result-detail.max-length = 2048 # maximum length of the stdout/stderr content to show, longer content will be truncated, default: 2048
# explanation of the following config:
# if the output is "aaa bbb ccc aaa", then the score will be: 0 - 20 - 10 - 5 - 20 = -55
keyword.score = 0 # base score, default: 0
keyword.keyword = [ "aaa", "bbb", "ccc" ] # list of keywords to match in stdout & stderr
keyword.weight = [ 20, 10, 5 ] # weight for each keyword, will be deducted for each keyword found
# similar to keyword, but will only match check name in clang-tidy
clangtidy.score = 0
clangtidy.keyword = [ "clang-diagnostic", "clang-analyzer", "misc", "performance", "portability" ]
clangtidy.weight = [ 5, 5, 5, 5, 5 ]
# similar to keyword, but will only match record ID & severity in cppcheck
cppcheck.score = 0
cppcheck.keyword = [ "error", "warning", "portability", "performance", "style" ]
cppcheck.weight = [ 15, 5, 5, 5, 5 ]
# similar to keyword, but will only match category in cpplint
cpplint.score = 0
cpplint.keyword = [ "runtime", "readability", "build" ]
cpplint.weight = [ 5, 20, 10 ]
# similar to keyword, but will only match kind in elf
elf.score = 0
elf.keyword = [ "Parentheses", "Length", "Arity", "Repetitive" ]
elf.weight = [ 100, 300, 50, 80 ]
# dummy parser, it will not parse the result from the command, always give the same output
dummy.score = 0 # score to add, default: 0
dummy.comment = "" # comment to show, default: ""
dummy.force-quit = false # whether to force quit the stage, default: false
diff.score = 5 # default score for each case, default: 5
diff.ignore-spaces = true # ignore spaces in diff, default: true
diff.hide = false # whether to hide the diff output, default: false
diff.force-quit = false # whether to force quit the stage if there is a difference, default: false
diff.max-length = 2048 # maximum length of the diff output, longer content will be truncated, default: 2048
diff.max-lines = 50 # maximum number of lines to show in the diff output, longer content will be truncated, default: 50
diff.hide-common-prefix = false # whether to hide the common prefix in the diff output, thus the first different line will be shown, default: false
# override when there are more than 1 cases in this stage
# for quality check stages, there is only 1 case so this is not needed
# previous fields without `case0.` prefix will be used as default for all cases
# and for run stages, multiple cases will be run with different inputs and outputs
# specific cases can be overridden here with these `case<x>.` prefix
# if no `case<x>.in` and `case<x>.out` is specified here,
# it will look for files with name `case<x>.in` and `case<x>.out`
# recursively in the directory of this toml file, and set them as corresponding
# `case<x>.in` and `case<x>.out` automatically, use the default value defined above
case0.in = "cases/case0.in" # file will be used as stdin, relative to this toml file
case0.out = "cases/case0.out" # file will be used to run diff with stdout, relative to this toml file
# the following fields just show the default values
case0.env = []
case0.command = ""
case0.files.import = []
case0.files.import-map = {}
case0.files.export = []
case0.copy-in-cwd = true
case0.limit.cpu = "1s"
case0.limit.mem = "256m"
case0.limit.time = "2s"
case0.limit.stdout = "32m"
case0.limit.stderr = "32m"
case0.limit.proc = 50
case0.diff.score = 5
case0.diff.ignore-spaces = true
case0.diff.hide = false
case0.diff.force-quit = false
case0.diff.max-length = 2048
case0.diff.max-lines = 50
case0.diff.hide-common-prefix = false
# the following 2 lines can be omitted as they can be detected automatically
# case1.in = "cases/case1.in"
# case1.out = "cases/case1.out"
# and you can only override part of the fields
case2.limit.cpu = "2s" # override CPU time limit for case2
case2.limit.mem = "512m" # override memory limit for case2
# also, you can put .in and .out files in other directories
# case3.in = "other/cases/case3.in"
# case3.out = "other/cases/case3.out"
# ===============================================
# ========== parser related config end ==========
# ===============================================
# all supported fields are listed here in 1 stage, but usually you need multiple stages
# for a real world example, please refer to playground repo

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] Filelength", "name": "[cq] Filelength",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -73,6 +72,7 @@
{ {
"name": "keyword", "name": "keyword",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -111,7 +111,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[cq] Filelength" name = "[cq] Filelength"

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399,
"effectiveUnixTimestamp": 1735487999,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] Filelength", "name": "[cq] Filelength",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -93,7 +92,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
release.end-time = 2024-12-30 23:59:59+08:00 time.end = 2024-12-30 23:59:59+08:00
release.begin-time = 2024-12-29 23:59:59+08:00 time.begin = 2024-12-29 23:59:59+08:00
[[stages]] [[stages]]
name = "[cq] Filelength" name = "[cq] Filelength"

View File

@ -1,3 +1,6 @@
import pytest
from pydantic import ValidationError
from tests.convert.utils import load_case from tests.convert.utils import load_case
@ -21,6 +24,23 @@ def test_diff() -> None:
load_case("diff") load_case("diff")
def test_elf() -> None:
load_case("elf")
def test_empty() -> None:
load_case("empty")
def test_extra_field() -> None:
with pytest.raises(ValidationError):
load_case("extra-field")
def test_full() -> None:
load_case("full")
def test_keyword() -> None: def test_keyword() -> None:
load_case("keyword") load_case("keyword")
@ -29,5 +49,9 @@ def test_result_detail() -> None:
load_case("result-detail") load_case("result-detail")
def test_elf() -> None: def test_unnecessary() -> None:
load_case("elf") load_case("unnecessary")
def test_whitelisted_chars() -> None:
load_case("whitelisted-chars")

View File

@ -1,18 +1,17 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051", "sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "", "sandboxToken": "",
"outputPath": "/tmp/joj3_result.json", "outputPath": "/tmp/joj3_result.json",
"preStages": [],
"stages": [ "stages": [
{ {
"name": "[cq] Filelength", "name": "[cq] Filelength",
"group": "cq", "groups": [
"cq"
],
"executor": { "executor": {
"name": "sandbox", "name": "sandbox",
"with": { "with": {
@ -90,7 +89,5 @@
] ]
} }
], ],
"preStages": [],
"postStages": [] "postStages": []
} }
}

View File

@ -1,5 +1,5 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name name = "hw7 ex2" # task name
[[stages]] [[stages]]
name = "[cq] Filelength" name = "[cq] Filelength"

View File

@ -0,0 +1 @@
health-check.whitelisted-chars = "你好!"

View File

@ -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",
"-whitelistedChars=你,好,",
"-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": {}
}
]
}
]
}

View File

@ -0,0 +1,2 @@
name = "health check"
max-total-score = 0

View File

0
tests/create/__init__.py Normal file
View File

View File

@ -1,4 +1,3 @@
[task]
name = "hw7 ex2" name = "hw7 ex2"
[[stages]] [[stages]]