Compare commits

..

47 Commits

Author SHA1 Message Date
ea894953ae
feat: set max-total-score = 0 for fallback conf 2025-07-01 11:21:56 -04:00
dcb2035be3
feat: try to find fallback conf with any name 2025-07-01 08:39:11 -04:00
8b16214be4
feat: automatically create default fallback conf 2025-07-01 08:29:12 -04:00
8f96115e6e
refactor: files.required -> health-check.required-files 2025-07-01 00:35:42 -04:00
7e6ee1b63e
fix: rebase error 2025-07-01 00:25:13 -04:00
dc7682a94f
refactor: move score, max-size, immutable-path to health-check 2025-07-01 00:20:37 -04:00
ba720ebe3f
fix: detect cases 2025-06-30 13:24:43 -04:00
88d864d49d
test: fix cases 2025-06-30 06:30:34 -04:00
7c764ab2cb
feat: task.name -> name in task.toml 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 2025-06-26 11:15:33 -04:00
c7b02f17a2
fix: unnecessary fields check alias 2025-06-26 08:28:16 -04:00
f4248a59a5
chore: result-detail.cpu alias 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 2025-06-26 06:28:29 -04:00
10a5efb293
feat: set log path based on suffix 2025-06-25 22:12:37 -04:00
c428d51706
feat: support --issue-label-exclusive 2025-06-25 05:00:20 -04:00
4d3f77d6d0
test: use modern time.begin 2025-06-25 03:49:08 -04:00
66551898be
test: fix cases 2025-06-25 03:46:28 -04:00
1313a24690
fix: change default immutable_path to make it backward compatible 2025-06-25 03:44:51 -04:00
087f62c304
feat: add unix timestamp back for backward compatibility 2025-06-25 02:42:37 -04:00
53395359cd
feat: files.import-map 2025-06-22 05:55:09 -04:00
d785216396 feat: diff.output.xxx -> diff.xxx ()
Reviewed-on: 
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 2025-06-21 12:25:58 -04:00
a49bc19b1e
chore: file_name -> filename 2025-06-21 12:07:09 -04:00
1b4637d01c
chore: sort glob result for stable output 2025-06-20 17:25:09 -04:00
875089cabd
fix: pdm run test 2025-06-20 17:18:00 -04:00
8d52febbc5
feat: new immutable dir () 2025-06-20 17:07:24 -04:00
fd7d09e7b2
fix: more intuitive default score 2025-06-20 11:54:51 -04:00
29952a9d2d
feat: support auto scoreboard name 2025-06-20 04:01:45 -04:00
2bcf4f8c60
feat: support issue label config in repo.toml 2025-06-20 03:40:16 -04:00
e2dc094263
feat: support penalty config 2025-06-19 06:43:29 -04:00
42bbf3ce39
feat: warn on immutable file not found 2025-06-18 10:14:29 -04:00
c6e2c63024
fix: cli time format 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 2025-06-18 09:13:34 -04:00
d8073e4eb6
fix: total score overwrite 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 2025-06-17 01:56:15 -04:00
340ba5d0c5
feat: more fields in result detail parser 2025-06-17 01:42:25 -04:00
ed43414b86
refactor: simplify group str generation 2025-06-16 10:09:29 -04:00
367a79850c
feat: support score in keyword parsers 2025-06-15 10:49:54 -04:00
5404313199
feat: move teapot settings to repo.toml 2025-06-15 02:05:15 -04:00
c6b833fbd5
fix: separated result status models 2025-06-14 05:52:06 -04:00
b14d83c37b
fix: make it work for joj3-forge-convert 2025-06-14 05:28:41 -04:00
2c1ad47f14
feat: get grading repo name from cwd 2025-06-14 05:21:35 -04:00
45 changed files with 823 additions and 283 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

@ -1,12 +1,8 @@
# JOJ3-forge # JOJ3-config-generator
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a98f9aa020874a93bc791a7616fccf21)](https://app.codacy.com/gh/joint-online-judge/JOJ3-config-generator/dashboard) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a98f9aa020874a93bc791a7616fccf21)](https://app.codacy.com/gh/joint-online-judge/JOJ3-config-generator/dashboard)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/a98f9aa020874a93bc791a7616fccf21)](https://app.codacy.com/gh/joint-online-judge/JOJ3-config-generator/dashboard) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/a98f9aa020874a93bc791a7616fccf21)](https://app.codacy.com/gh/joint-online-judge/JOJ3-config-generator/dashboard)
## Introduction
JOJ3-forge is a CLI tool that generates configuration files for [JOJ3](https://github.com/joint-online-judge/JOJ3).
## Getting Started ## Getting Started
### For users ### For users
@ -14,7 +10,7 @@ JOJ3-forge is a CLI tool that generates configuration files for [JOJ3](https://g
1. Install [Python>=3.9](https://www.python.org/) and [pip](https://pip.pypa.io/) 1. Install [Python>=3.9](https://www.python.org/) and [pip](https://pip.pypa.io/)
2. (Optional) Create a virtual environment, check [here](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/). 2. (Optional) Create a virtual environment, check [here](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/).
3. Install/Upgrade the project by `pip install --force-reinstall --upgrade git+ssh://git@focs.ji.sjtu.edu.cn:2222/JOJ/JOJ3-config-generator.git` 3. Install/Upgrade the project by `pip install --force-reinstall --upgrade git+ssh://git@focs.ji.sjtu.edu.cn:2222/JOJ/JOJ3-config-generator.git`
4. Run it by `joj3-forge --help` 4. Run it by `joj3-config-generator --help`
### For developers ### For developers
@ -27,62 +23,22 @@ JOJ3-forge is a CLI tool that generates configuration files for [JOJ3](https://g
## How to use? ## How to use?
Run `joj3-forge --help` to get basic CLI usage information. - `joj3-config-generator convert` function is now supported, currently support one argument as input, it indicates the **convert root**
- default value on the server can be given as `/home/tt/.config/joj`
### `convert`
- `joj3-forge convert` function is now supported, currently support one argument as input, it indicates the **convert root*
- default value on the server should be given as `/home/tt/.config/joj`
- **NOTE:** the user should ensure that the ideal `repo.toml` file is in the sub-directory of the **convert root** - **NOTE:** the user should ensure that the ideal `repo.toml` file is in the sub-directory of the **convert root**
- the intended immutable files should be placed at a sub-directory named `immutable_files` at same position as the `repo.toml` file - the intended immutable files should be placed at a sub-directory named `immutable_files` at same position as the `repo.toml` file
- a sample directory tree as follows
```shell ```shell
$ tree -a $ tree .
home .
`-- tt |- immutable_files
|-- .cache | |-- push.yaml
`-- .config | |-- release.yaml
`-- joj |-- repo.toml
|-- hidden
| |-- ex1
| | |-- case1.in
| | |-- case1.out
| | |-- conf.json
| | `-- conf.toml
| |-- immutable_files
| | |-- push.yaml
| | `-- release.yaml
| |-- p1
| | |-- case1.in
| | |-- case1.out
| | |-- conf.json
| | `-- conf.toml
| `-- repo.toml
|-- students
| |-- ex1
| | |-- case1.in
| | |-- case1.out
| | |-- conf.json
| | `-- conf.toml
| |-- immutable_files
| | |-- push.yaml
| | `-- release.yaml
| |-- p1
| | |-- case1.in
| | |-- case1.out
| | |-- conf.json
| | `-- conf.toml
| `-- repo.toml
|-- students.csv
`-- tools
|-- .clang-tidy
|-- compile
`-- helper.sh
``` ```
- sample command on the server - sample command on the server
```shell ```shell
joj3-forge convert /home/tt/.config/joj joj3-config-generator convert /home/tt/.config/joj
``` ```

View File

@ -2,7 +2,11 @@ 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, 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,
)
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 (
@ -23,9 +27,10 @@ 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),
# TODO: remove these 2 fields, will be handled in the health check stage
expire_unix_timestamp=int(task_conf.release.end_time.timestamp()), expire_unix_timestamp=int(task_conf.release.end_time.timestamp()),
effective_unix_timestamp=int(task_conf.release.begin_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

View File

@ -5,7 +5,7 @@ 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
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
@ -83,8 +83,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]

View File

@ -95,6 +95,12 @@ 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()}")
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 = "invalid commit"\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

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,8 +1,10 @@
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, BaseModel, Field, field_validator, model_validator
from joj3_config_generator.models.common import Memory
class Files(BaseModel): class Files(BaseModel):
@ -20,42 +22,115 @@ class Groups(BaseModel):
) )
class Label(BaseModel):
name: str = "Kind/Testing"
color: str = "#795548"
exclusive: bool = False
class Issue(BaseModel):
label: Label = Label()
show_submitter: bool = Field(
True, validation_alias=AliasChoices("show-submitter", "show_submitter")
)
class HealthCheck(BaseModel):
score: int = 0
max_size: int = Field(
Memory("10m"), validation_alias=AliasChoices("max-size", "max_size")
)
immutable_path: Path = Field(
Path("immutable"),
validation_alias=AliasChoices("immutable-path", "immutable_path"),
)
required_files: List[str] = Field(
[], validation_alias=AliasChoices("required-files", "required_files")
)
@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}')
class Config(BaseModel): class Config(BaseModel):
max_size: float = Field( root: Path = Field(Path("."), exclude=True)
10, ge=0, validation_alias=AliasChoices("max-size", "max_size") path: Path = Field(Path("repo.toml"), exclude=True)
)
files: Files = Files()
sandbox_token: str = Field(
"", validation_alias=AliasChoices("sandbox-token", "sandbox_token")
)
max_total_score: int = Field(
100, validation_alias=AliasChoices("max-total-score", "max_total_score")
)
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,
)
grading_repo_name: str = Field(
"",
validation_alias=AliasChoices("grading-repo-name", "grading_repo_name"),
)
sandbox_token: str = Field(
"", validation_alias=AliasChoices("sandbox-token", "sandbox_token")
)
max_total_score: int = Field(
100, validation_alias=AliasChoices("max-total-score", "max_total_score")
) )
groups: Groups = Groups() groups: Groups = Groups()
root: Path = Path(".") issue: Issue = Issue()
path: Path = Path("repo.toml")
grading_repo_name: str = Field( health_check: HealthCheck = Field(
f"{socket.gethostname().split('-')[0]}-joj", HealthCheck(), validation_alias=AliasChoices("health-check", "health_check")
validation_alias=AliasChoices("grading-repo-name", "grading_repo_name"), )
# TODO: remove files, max_size, health_check_score, and immutable_path in the future
files: Files = Files()
max_size: float = Field(
10, ge=0, validation_alias=AliasChoices("max-size", "max_size")
) )
health_check_score: int = Field( health_check_score: int = Field(
0, validation_alias=AliasChoices("health-check-score", "health_check_score") 0, validation_alias=AliasChoices("health-check-score", "health_check_score")
) )
submitter_in_issue_title: bool = Field( immutable_path: Path = Field(
True, Path("immutable"),
validation_alias=AliasChoices( validation_alias=AliasChoices("immutable-path", "immutable_path"),
"submitter-in-issue-title", "submitter_in_issue_title"
),
) )
# TODO: remove gitea_token and gitea_org in the future
gitea_token: str = Field(
"", validation_alias=AliasChoices("gitea-token", "gitea_token")
)
gitea_org: str = Field("", validation_alias=AliasChoices("gitea-org", "gitea_org"))
@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
# TODO: remove this validator in the future
@model_validator(mode="after")
def set_health_check(self) -> "Config":
if "health_check_score" in self.model_fields_set:
self.health_check.score = self.health_check_score
if "max_size" in self.model_fields_set:
self.health_check.max_size = Memory(f"{self.max_size}m")
if "immutable_path" in self.model_fields_set:
self.health_check.immutable_path = self.immutable_path
if (
"files" in self.model_fields_set
and "required" in self.files.model_fields_set
):
self.health_check.required_files = self.files.required
return self

View File

@ -177,6 +177,12 @@ class Config(BaseModel):
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 +192,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 +223,7 @@ class KeywordConfig(BaseModel):
class KeywordMatchConfig(BaseModel): class KeywordMatchConfig(BaseModel):
score: int = 0
matches: List[KeywordConfig] = [] matches: List[KeywordConfig] = []

View File

@ -1,7 +1,8 @@
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, AliasChoices,
@ -15,6 +16,7 @@ from pydantic import (
from joj3_config_generator.models.common import Memory, Time from joj3_config_generator.models.common import Memory, 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,
@ -25,9 +27,9 @@ from joj3_config_generator.models.repo import Groups
class ParserResultDetail(BaseModel): class ParserResultDetail(BaseModel):
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,6 +40,12 @@ 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(BaseModel):
@ -58,7 +66,16 @@ class ParserDummy(BaseModel):
) )
class ParserResultStatus(BaseModel):
comment: str = ""
score: int = 0
force_quit: bool = Field(
True, validation_alias=AliasChoices("force-quit", "force_quit")
)
class ParserKeyword(BaseModel): class ParserKeyword(BaseModel):
score: int = 0
keyword: List[str] = [] keyword: List[str] = []
weight: List[int] = [] weight: List[int] = []
@ -82,40 +99,75 @@ class ParserDiffOutputs(BaseModel):
class ParserDiff(BaseModel): class ParserDiff(BaseModel):
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(BaseModel):
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")
)
class Limit(BaseModel): class Limit(BaseModel):
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):
@ -141,18 +193,17 @@ 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] = []
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 +215,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] = {}
@ -195,17 +247,52 @@ class Release(BaseModel):
) # timestamp = 0, no begin time ) # timestamp = 0, no begin time
class SubmissionTime(BaseModel):
begin: Optional[datetime] = None
end: Optional[datetime] = None
class Task(BaseModel): class Task(BaseModel):
name: str = "unknown" name: str = "unknown"
class Penalties(BaseModel):
hours: List[float] = []
factors: List[float] = []
class Config(BaseModel): class Config(BaseModel):
root: Path = Path(".") root: Path = Field(Path("."), exclude=True)
path: Path = Path("task.toml") path: Path = Field(Path("task.toml"), exclude=True)
task: Task = Task() # Task name (e.g., hw3 ex5) suffix: str = Field("", exclude=True)
release: Release = Release() # Release configuration
stages: List[Stage] = [] # list of stage configurations task: Task = Task() # TODO: remove it in the future
groups: Groups = Groups() name: str = "unknown" # Task name (e.g., hw3 ex5)
max_total_score: int = Field( max_total_score: Optional[int] = Field(
100, validation_alias=AliasChoices("max-total-score", "max_total_score") None, validation_alias=AliasChoices("max-total-score", "max_total_score")
) )
scoreboard: str = "scoreboard.csv"
time: SubmissionTime = SubmissionTime() # Valid time configuration
release: Release = Release() # Release configuration
groups: Groups = Groups()
penalties: Penalties = Penalties()
stages: List[Stage] = [] # list of stage configurations
# TODO: remove this validator in the future
@model_validator(mode="after")
def set_name(self) -> "Config":
if "task" in self.model_fields_set and "name" in self.task.model_fields_set:
self.name = self.task.name
return self
@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,26 @@
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(repo_conf: repo.Config) -> List[str]:
res = [
f"REPOS_DIR={CACHE_ROOT}",
f"LOG_FILE_PATH={TEAPOT_LOG_PATH}",
]
if repo_conf.gitea_org:
res.append(f"GITEA_ORG_NAME={repo_conf.gitea_org}")
if repo_conf.gitea_token:
res.append(f"GITEA_ACCESS_TOKEN={repo_conf.gitea_token}")
return res
def get_teapot_post_stage( def get_teapot_post_stage(
@ -17,13 +34,36 @@ 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 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 +72,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(repo_conf),
cpu_limit=common.Time("30s"), cpu_limit=common.Time("30s"),
clock_limit=common.Time("60s"), clock_limit=common.Time("60s"),
), ),
@ -47,14 +87,47 @@ def get_teapot_post_stage(
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 / "immutable_files"
immutable_files = []
file_sums = []
file_names = []
for file in repo_conf.files.immutable:
file_path = immutable_dir / Path(file).name
if not file_path.exists():
logger.warning(f"Immutable file not found: {file_path}")
continue
immutable_files.append(file_path)
file_sums.append(calc_sha256sum(file_path))
file_names.append(file)
immutable_dir = (
repo_conf.root / repo_conf.path
).parent / repo_conf.health_check.immutable_path
if not immutable_dir.exists():
return file_sums, file_names
file_sums = []
file_names = []
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_names.append(file_path.relative_to(immutable_dir).as_posix())
return file_sums, file_names
def get_health_check_args(repo_conf: repo.Config) -> List[str]: def get_health_check_args(repo_conf: repo.Config) -> List[str]:
file_sums, file_names = get_check_lists(repo_conf)
return [ return [
"/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"-checkFileSumList={','.join(file_sums)}",
f"-checkFileNameList={','.join(repo_conf.files.immutable)}", f"-checkFileNameList={','.join(file_names)}",
] ]
@ -65,28 +138,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:
group_config_str = ",".join( group_str = lambda groups: ",".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: group_config = group_str(
overwrite_group_config_str = ",".join( task_conf.groups if task_conf.groups.name else repo_conf.groups
f"{name}={max_count}:{time_period}" )
for name, max_count, time_period in zip( res.extend(["--group-config", group_config])
task_conf.groups.name, if task_conf.time.begin:
task_conf.groups.max_count, res.extend(["--begin-time", task_conf.time.begin.strftime("%Y-%m-%dT%H:%M:%S")])
task_conf.groups.time_period_hour, if task_conf.time.end:
) res.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
) )
res.extend(["--group-config", overwrite_group_config_str]) )
else: res.extend(["--penalty-config", penalty_config])
res.extend(["--group-config", group_config_str])
return res return res
@ -100,7 +179,8 @@ def get_health_check_stage(
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 +188,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(repo_conf),
), ),
], ],
), ),
@ -116,7 +196,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 +210,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,10 @@ 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, Tuple, Union
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.const import DEFAULT_PATH_ENV, JOJ3_CONFIG_ROOT
from joj3_config_generator.models.const import (
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
@ -61,7 +56,7 @@ def get_parser_handler_map(
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.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 +74,38 @@ 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
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},
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 +124,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 +151,29 @@ 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_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,
) )
@ -180,11 +193,7 @@ def fix_diff(
# 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, task_stage.cases)
# cases specified in toml config but not skipped # cases specified in toml config but not skipped
specified_cases = [ specified_cases = [(case, task_stage.cases[case]) for case in 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: for case_name, case in specified_cases:
@ -200,7 +209,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,
) )
@ -215,18 +224,24 @@ def fix_diff(
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) 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"),
) )
] ]
) )
@ -234,18 +249,20 @@ def fix_diff(
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) stage_cases.append(cmd)
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=str(base_dir / f"{case_name}.out"),
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,
) )
] ]
) )
@ -287,16 +304,22 @@ def get_unspecified_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_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
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
continue 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
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

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,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 10245871, "maxTotalScore": 10245871,
"stage": { "stage": {
@ -64,8 +64,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 +75,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"
] ]
} }
@ -136,6 +145,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": {},
@ -260,6 +272,7 @@
{ {
"name": "keyword", "name": "keyword",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -368,6 +381,7 @@
{ {
"name": "clangtidy", "name": "clangtidy",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -498,12 +512,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 +542,7 @@
{ {
"name": "clangtidy", "name": "clangtidy",
"with": { "with": {
"score": 0,
"matches": [] "matches": []
} }
}, },
@ -551,6 +568,7 @@
{ {
"name": "cpplint", "name": "cpplint",
"with": { "with": {
"score": 0,
"matches": [] "matches": []
} }
}, },
@ -559,7 +577,7 @@
"with": { "with": {
"score": 0, "score": 0,
"comment": "", "comment": "",
"forceQuitOnNotAccepted": false "forceQuitOnNotAccepted": true
} }
}, },
{ {
@ -573,7 +591,7 @@
"with": { "with": {
"score": 0, "score": 0,
"comment": "", "comment": "",
"forceQuitOnNotAccepted": false "forceQuit": false
} }
}, },
{ {
@ -651,6 +669,7 @@
{ {
"name": "cpplint", "name": "cpplint",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [
@ -722,7 +741,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 +768,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 +796,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
}
]
}
]
} }
}, },
{ {
@ -798,9 +883,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": {

View File

@ -2,8 +2,13 @@
task.name = "hw7 ex2" # task name task.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,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {
@ -78,6 +78,7 @@
{ {
"name": "clangtidy", "name": "clangtidy",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {
@ -71,6 +71,7 @@
{ {
"name": "cppcheck", "name": "cppcheck",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {
@ -70,6 +70,7 @@
{ {
"name": "cpplint", "name": "cpplint",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {
@ -144,7 +144,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 +159,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,
@ -174,7 +174,7 @@
"outputs": [ "outputs": [
{ {
"score": 1232131, "score": 1232131,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/case9.out", "answerPath": "/home/tt/.config/joj/diff/case9.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -189,7 +189,7 @@
"outputs": [ "outputs": [
{ {
"score": 92321, "score": 92321,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case11.out", "answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case11.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -204,7 +204,7 @@
"outputs": [ "outputs": [
{ {
"score": 823131, "score": 823131,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case10.out", "answerPath": "/home/tt/.config/joj/diff/task1/subtask1/case10.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -219,7 +219,7 @@
"outputs": [ "outputs": [
{ {
"score": 2590, "score": 2590,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case5.out", "answerPath": "/home/tt/.config/joj/diff/task1/case5.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -234,7 +234,7 @@
"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,
@ -249,7 +249,7 @@
"outputs": [ "outputs": [
{ {
"score": 100, "score": 100,
"fileName": "stdout", "filename": "stdout",
"answerPath": "/home/tt/.config/joj/diff/task1/case4.out", "answerPath": "/home/tt/.config/joj/diff/task1/case4.out",
"compareSpace": false, "compareSpace": false,
"alwaysHide": false, "alwaysHide": false,
@ -264,7 +264,7 @@
"outputs": [ "outputs": [
{ {
"score": 100, "score": 100,
"fileName": "stdout", "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 +279,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 +294,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,

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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,26 @@ 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

View File

@ -1,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {
@ -74,6 +74,7 @@
{ {
"name": "elf", "name": "elf",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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,182 @@
{
"name": "invalid commit",
"logPath": "/home/tt/.cache/joj3/invalid/joj3.log",
"expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100,
"stage": {
"sandboxExecServer": "172.17.0.1:5051",
"sandboxToken": "",
"outputPath": "/tmp/joj3_result.json",
"stages": [
{
"name": "Health Check",
"group": "",
"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": {}
}
]
}
],
"preStages": [],
"postStages": [
{
"name": "teapot",
"group": "",
"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": {
"msg": "joj3 summary"
}
},
{
"name": "debug",
"with": {}
}
]
}
]
}
}

View File

@ -0,0 +1,2 @@
name = "invalid commit"
max-total-score = 0

View File

@ -1,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {
@ -73,6 +73,7 @@
{ {
"name": "keyword", "name": "keyword",
"with": { "with": {
"score": 0,
"matches": [ "matches": [
{ {
"keywords": [ "keywords": [

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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,8 +1,8 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 1735574399, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 1735487999, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",
"maxTotalScore": 100, "maxTotalScore": 100,
"stage": { "stage": {

View File

@ -1,8 +1,8 @@
# general task configuration # general task configuration
task.name = "hw7 ex2" # task name task.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

@ -21,6 +21,14 @@ 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_keyword() -> None: def test_keyword() -> None:
load_case("keyword") load_case("keyword")
@ -29,5 +37,5 @@ 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")

View File

@ -1,6 +1,6 @@
{ {
"name": "hw7 ex2", "name": "hw7 ex2",
"logPath": "/home/tt/.cache/joj3/joj3.log", "logPath": "/home/tt/.cache/joj3/hw7/joj3.log",
"expireUnixTimestamp": 0, "expireUnixTimestamp": 0,
"effectiveUnixTimestamp": 0, "effectiveUnixTimestamp": 0,
"actorCsvPath": "/home/tt/.config/joj/students.csv", "actorCsvPath": "/home/tt/.config/joj/students.csv",

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