Skip to content

Commit d7ae6ba

Browse files
Merge pull request #347 from webtech-network/test-harnessing
refactor: overhaul unit tests and achieve 100% coverage on core modules
2 parents 493107a + 305de7a commit d7ae6ba

34 files changed

Lines changed: 434 additions & 74 deletions

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ jobs:
3232
- name: Analysing the code with pylint
3333
if: steps.changed-files.outputs.files != ''
3434
run: |
35-
pylint ${{ steps.changed-files.outputs.files }}
35+
PYTHONPATH=. pylint ${{ steps.changed-files.outputs.files }}

autograder/autograder.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def run(self, submission: Submission):
7474
)
7575
break
7676
logger.info("Step %s completed successfully (external_user_id=%s)", step_name, submission.user_id)
77-
except Exception as e:
77+
except Exception as e: # pylint: disable=broad-exception-caught
7878
pipeline_execution.status = PipelineStatus.INTERRUPTED
7979
logger.error(
8080
"Unhandled exception in step %s (external_user_id=%s): %s",
@@ -84,7 +84,6 @@ def run(self, submission: Submission):
8484
exc_info=True,
8585
)
8686
break
87-
8887
pipeline_execution.finish_execution() # Generates GradingResult object in pipeline execution
8988

9089
# Cleanup: Destroy sandbox if it was created
@@ -113,7 +112,7 @@ def _cleanup_sandbox(self, pipeline_execution: PipelineExecution) -> None:
113112
pipeline_execution.submission.user_id,
114113
language.value if language else "none",
115114
)
116-
except Exception as e:
115+
except Exception as e: # pylint: disable=broad-exception-caught
117116
# Log error but don't fail the pipeline
118117
logger.warning(
119118
"Failed to cleanup sandbox (external_user_id=%s): %s",
@@ -122,7 +121,7 @@ def _cleanup_sandbox(self, pipeline_execution: PipelineExecution) -> None:
122121
)
123122

124123

125-
def build_pipeline(
124+
def build_pipeline( # pylint: disable=too-many-arguments,too-many-locals
126125
template_name,
127126
include_feedback,
128127
grading_criteria,
@@ -160,7 +159,7 @@ def build_pipeline(
160159
elif template_name:
161160
# Normalize template names (can be string, comma-separated string, or list)
162161
from autograder.steps.load_template_step import TemplateLoaderStep
163-
names = TemplateLoaderStep._normalize_template_names(template_name)
162+
names = TemplateLoaderStep.normalize_template_names(template_name)
164163
for name in names:
165164
templates.append(template_service.load_builtin_template(name))
166165

autograder/models/abstract/ai_test_function.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from autograder.models.abstract.test_function import TestFunction
55
from autograder.models.dataclass.submission import SubmissionFile
66
from autograder.models.dataclass.test_result import TestResult
7+
from autograder.utils.executors.ai_executor import AiExecutor, TestInput
78
from sandbox_manager.sandbox_container import SandboxContainer
89

910

@@ -35,6 +36,7 @@ def execute(
3536
self,
3637
files: Optional[List[SubmissionFile]],
3738
sandbox: Optional[SandboxContainer],
39+
*args,
3840
**kwargs,
3941
) -> TestResult:
4042
"""
@@ -45,15 +47,18 @@ def execute(
4547
test name in that dict and returns the result directly. If the dict is
4648
absent (standalone usage), it falls back to ``_run_single()``.
4749
"""
50+
# args is not used in AI tests as parameters are in kwargs
51+
_ = args
4852
pre_computed: Optional[Dict[str, TestResult]] = kwargs.get("pre_computed_results")
4953
if pre_computed is not None and self.name in pre_computed:
5054
return pre_computed[self.name]
5155

52-
return self._run_single(files, **kwargs)
56+
return self._run_single(files, *args, **kwargs)
5357

5458
def _run_single(
5559
self,
5660
files: Optional[List[SubmissionFile]],
61+
*args,
5762
**kwargs,
5863
) -> TestResult:
5964
"""
@@ -62,9 +67,7 @@ def _run_single(
6267
Used when ``pre_computed_results`` is not available (e.g. unit tests,
6368
standalone runners, or pipelines that do not include ``AiBatchStep``).
6469
"""
65-
# Local import to avoid circular dependencies.
66-
from autograder.utils.executors.ai_executor import AiExecutor, TestInput
67-
70+
_ = args
6871
locale: str = kwargs.get("locale", "en")
6972
prompt = self.build_prompt(files, **kwargs)
7073
submission_files = {f.filename: f.content for f in files} if files else {}

autograder/models/abstract/criteria_tree_processer.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66

77

88
class CriteriaTreeProcesser(ABC):
9+
"""
10+
Abstract base class for processing the criteria tree structure.
11+
"""
912
@abstractmethod
1013
def process_subject(self, subject: "SubjectNode") -> Any:
11-
pass
14+
"""Process a subject node in the criteria tree."""
1215

1316
@abstractmethod
1417
def process_test(self, test: "TestNode") -> Any:
15-
pass
18+
"""Process a test node in the criteria tree."""
1619

1720
@abstractmethod
1821
def process_category(self, category: "CategoryNode") -> Any:
19-
pass
22+
"""Process a category node in the criteria tree."""

autograder/models/abstract/step.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010

1111
class Step(ABC):
12+
"""
13+
Abstract base class for all pipeline steps.
14+
"""
1215
@property
1316
@abstractmethod
1417
def step_name(self) -> StepName:
1518
"""Return the name of the step (e.g. StepName.GRADE)."""
16-
pass
1719

1820
def execute(self, pipeline_exec: PipelineExecution) -> PipelineExecution:
1921
"""
@@ -24,7 +26,7 @@ def execute(self, pipeline_exec: PipelineExecution) -> PipelineExecution:
2426
locale = pipeline_exec.locale
2527
try:
2628
return self._execute(pipeline_exec)
27-
except Exception as e:
29+
except Exception as e: # pylint: disable=broad-exception-caught
2830
logger.exception("Step %s was interrupted: %s", self.step_name.value, str(e))
2931

3032
error_msg = t("system.error.step_interrupted", locale=locale, error=str(e))
@@ -42,5 +44,4 @@ def execute(self, pipeline_exec: PipelineExecution) -> PipelineExecution:
4244
@abstractmethod
4345
def _execute(self, pipeline_exec: PipelineExecution) -> PipelineExecution:
4446
"""Internal execution logic to be implemented by subclasses."""
45-
pass
4647

autograder/models/config/category.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
class CategoryConfig(BaseModel):
9+
"""Configuration for a grading category (base, bonus, or penalty)."""
910
weight: float = Field(
1011
..., ge=0, le=100, description="Weight of this category (0-100)"
1112
)

autograder/models/config/criteria.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class CriteriaConfig(BaseModel):
99

1010
test_library: Optional[str] = Field(
1111
None, description="Name of the test library/template to use"
12-
) # TODO -> Remove this attribute (it already sits in grading config)
12+
)
1313
base: CategoryConfig = Field(..., description="Base grading criteria (required)")
1414
bonus: Optional[CategoryConfig] = Field(None, description="Bonus points criteria")
1515
penalty: Optional[CategoryConfig] = Field(None, description="Penalty criteria")

autograder/models/config/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class AssetConfig(BaseModel):
1111
@field_validator('source')
1212
@classmethod
1313
def validate_source(cls, v: str) -> str:
14+
"""Validate the source path of the asset."""
1415
if not v:
1516
raise ValueError("source must not be empty")
1617
if v.startswith('/'):
@@ -22,6 +23,7 @@ def validate_source(cls, v: str) -> str:
2223
@field_validator('target')
2324
@classmethod
2425
def validate_target(cls, v: str) -> str:
26+
"""Validate the target path of the asset."""
2527
if not v:
2628
raise ValueError("target must not be empty")
2729
if not v.startswith('/'):

autograder/models/config/subject.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55

66
class SubjectConfig(BaseModel):
7+
"""Configuration for a subject within a grading category."""
78
subject_name: str = Field(..., description="Name of the subject")
89
weight: float = Field(
910
..., ge=0, le=100, description="Weight of this subject (0-100)"

autograder/models/config/test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def get_args_list(self) -> List[Any]:
3939
def get_kwargs_dict(self) -> Dict[str, Any]:
4040
"""Convert named parameters to keyword arguments dictionary."""
4141
kwargs = {}
42-
if self.parameters:
42+
if self.parameters is not None:
4343
kwargs.update({param.name: param.value for param in self.parameters})
4444

4545
if self.model_extra:

0 commit comments

Comments
 (0)