Skip to content

Commit f8fd9cb

Browse files
Merge pull request #253 from webtech-network/234-td-separate-pipelineexecution-summary-logic
2 parents 48821e7 + 4e81465 commit f8fd9cb

10 files changed

Lines changed: 286 additions & 173 deletions

File tree

autograder/models/dataclass/step_result.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class StepResult(Generic[T]):
3030
data: T
3131
status: StepStatus = StepStatus.SUCCESS
3232
error: Optional[str] = None
33+
error_data: Any = None
3334
original_input: Any = None
3435

3536
@property
@@ -43,6 +44,6 @@ def success(cls, step: StepName, data: T) -> "StepResult[T]":
4344
return cls(step=step, data=data, status=StepStatus.SUCCESS)
4445

4546
@classmethod
46-
def fail(cls, step: StepName, error: str, data: Optional[T] = None) -> "StepResult[T]":
47+
def fail(cls, step: StepName, error: str, data: Optional[T] = None, error_data: Any = None) -> "StepResult[T]":
4748
"""Creates a failed StepResult."""
48-
return cls(step=step, data=data, status=StepStatus.FAIL, error=error)
49+
return cls(step=step, data=data, status=StepStatus.FAIL, error=error, error_data=error_data)

autograder/models/pipeline_execution.py

Lines changed: 0 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -44,151 +44,7 @@ def add_step_result(self, step_result: StepResult) -> 'PipelineExecution':
4444
self.step_results.append(step_result)
4545
return self
4646

47-
def get_pipeline_execution_summary(self) -> Dict[str, Any]:
48-
"""
49-
Generate a detailed summary of the pipeline execution for API responses.
50-
51-
Returns:
52-
Dictionary containing execution status, steps, and error details
53-
"""
54-
execution_time_ms = int((time.time() - self.start_time) * 1000) if self.start_time else 0
55-
56-
# Determine failed step if any
57-
failed_step = None
58-
for step in self.step_results:
59-
if step.status == StepStatus.FAIL:
60-
failed_step = step.step.value
61-
break
62-
63-
# Build step details
64-
steps = []
65-
for step in self.step_results:
66-
if step.step == StepName.BOOTSTRAP:
67-
continue # Skip bootstrap step in output
68-
69-
step_info = {
70-
"name": step.step.value,
71-
"status": step.status.value,
72-
}
73-
74-
# Add execution time if available
75-
if hasattr(step, 'execution_time_ms'):
76-
step_info["execution_time_ms"] = step.execution_time_ms
77-
78-
# Add success message for completed steps
79-
if step.status == StepStatus.SUCCESS:
80-
if step.step == StepName.LOAD_TEMPLATE:
81-
step_info["message"] = "Template loaded successfully"
82-
elif step.step == StepName.BUILD_TREE:
83-
step_info["message"] = "Criteria tree built successfully"
84-
elif step.step == StepName.PRE_FLIGHT:
85-
step_info["message"] = "All preflight checks passed"
86-
elif step.step == StepName.GRADE:
87-
if self.result and self.result.final_score is not None:
88-
step_info["message"] = f"Grading completed: {self.result.final_score}/100"
89-
else:
90-
step_info["message"] = "Grading completed"
91-
elif step.step == StepName.FEEDBACK:
92-
step_info["message"] = "Feedback generated"
93-
elif step.step == StepName.EXPORTER:
94-
step_info["message"] = "Results exported"
95-
96-
# Add error details for failed steps
97-
elif step.status == StepStatus.FAIL and step.error:
98-
step_info["message"] = step.error.split('\n')[0] # First line of error
99-
step_info["error_details"] = self._extract_error_details(step)
100-
101-
steps.append(step_info)
102-
103-
# Count planned vs completed steps
104-
total_planned = 7 # BOOTSTRAP, LOAD_TEMPLATE, BUILD_TREE, PRE_FLIGHT, GRADE, FEEDBACK, EXPORT
105-
completed = len([s for s in self.step_results if s.step != StepName.BOOTSTRAP])
106-
107-
return {
108-
"status": self.status.value if self.status != PipelineStatus.EMPTY else "unknown",
109-
"failed_at_step": failed_step,
110-
"total_steps_planned": total_planned,
111-
"steps_completed": completed,
112-
"execution_time_ms": execution_time_ms,
113-
"steps": steps
114-
}
115-
116-
def _extract_error_details(self, step: StepResult) -> Dict[str, Any]:
117-
"""Extract structured error details from a failed step."""
118-
error_details = {}
119-
120-
if step.step == StepName.PRE_FLIGHT:
121-
# Parse preflight errors
122-
error_text = step.error
123-
124-
# Check if it's a missing file error
125-
if "Arquivo ou diretório obrigatório não encontrado" in error_text or "Required file" in error_text:
126-
error_details["error_type"] = "required_file_missing"
127-
error_details["phase"] = "required_files"
128-
# Extract file name if possible
129-
if "`'" in error_text:
130-
start = error_text.find("`'") + 2
131-
end = error_text.find("'`", start)
132-
if end > start:
133-
error_details["missing_file"] = error_text[start:end]
134-
135-
# Check if it's a setup command error
136-
elif "Setup command" in error_text:
137-
error_details["error_type"] = "setup_command_failed"
138-
error_details["phase"] = "setup_commands"
139-
140-
# Extract command name
141-
if "Setup command '" in error_text:
142-
start = error_text.find("Setup command '") + 15
143-
end = error_text.find("' failed", start)
144-
if end > start:
145-
error_details["command_name"] = error_text[start:end]
146-
147-
# Extract command
148-
if "**Command:** `" in error_text:
149-
start = error_text.find("**Command:** `") + 14
150-
end = error_text.find("`", start)
151-
if end > start:
152-
if "failed_command" not in error_details:
153-
error_details["failed_command"] = {}
154-
error_details["failed_command"]["command"] = error_text[start:end]
155-
156-
# Extract exit code
157-
if "exit code " in error_text:
158-
try:
159-
start = error_text.find("exit code ") + 10
160-
end = start
161-
while end < len(error_text) and error_text[end].isdigit():
162-
end += 1
163-
if end > start:
164-
if "failed_command" not in error_details:
165-
error_details["failed_command"] = {}
166-
error_details["failed_command"]["exit_code"] = int(error_text[start:end])
167-
except (ValueError, IndexError):
168-
pass
169-
170-
# Extract stderr
171-
if "**Error Output (stderr):**" in error_text:
172-
start = error_text.find("**Error Output (stderr):**") + 26
173-
# Find the code block
174-
start = error_text.find("```", start) + 3
175-
end = error_text.find("```", start)
176-
if end > start:
177-
if "failed_command" not in error_details:
178-
error_details["failed_command"] = {}
179-
error_details["failed_command"]["stderr"] = error_text[start:end].strip()
180-
181-
# Extract stdout if present
182-
if "**Output (stdout):**" in error_text:
183-
start = error_text.find("**Output (stdout):**") + 20
184-
start = error_text.find("```", start) + 3
185-
end = error_text.find("```", start)
186-
if end > start:
187-
if "failed_command" not in error_details:
188-
error_details["failed_command"] = {}
189-
error_details["failed_command"]["stdout"] = error_text[start:end].strip()
19047

191-
return error_details
19248

19349
def get_step_result(self, step_name: StepName) -> StepResult:
19450
for step_result in self.step_results:
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import time
2+
from typing import Dict, Any
3+
4+
from autograder.models.dataclass.step_result import StepResult, StepName, StepStatus
5+
from autograder.models.pipeline_execution import PipelineExecution, PipelineStatus
6+
from autograder.models.dataclass.preflight_error import PreflightCheckType
7+
8+
9+
class PipelineExecutionSerializer:
10+
"""
11+
Handles the serialization of PipelineExecution into dictionary payloads
12+
suitable for API responses and analytical views.
13+
"""
14+
15+
@classmethod
16+
def serialize(cls, execution: PipelineExecution) -> Dict[str, Any]:
17+
"""
18+
Generate a detailed summary of the pipeline execution for API responses.
19+
20+
Returns:
21+
Dictionary containing execution status, steps, and error details
22+
"""
23+
execution_time_ms = int((time.time() - execution.start_time) * 1000) if execution.start_time else 0
24+
25+
# Determine failed step if any
26+
failed_step = None
27+
for step in execution.step_results:
28+
if step.status == StepStatus.FAIL:
29+
failed_step = step.step.value
30+
break
31+
32+
# Build step details
33+
steps = []
34+
for step in execution.step_results:
35+
if step.step == StepName.BOOTSTRAP:
36+
continue # Skip bootstrap step in output
37+
38+
step_info = {
39+
"name": step.step.value,
40+
"status": step.status.value,
41+
}
42+
43+
# Add execution time if available
44+
if hasattr(step, 'execution_time_ms'):
45+
step_info["execution_time_ms"] = step.execution_time_ms
46+
47+
# Add success message for completed steps
48+
if step.status == StepStatus.SUCCESS:
49+
step_info["message"] = cls._get_success_message(step, execution)
50+
51+
# Add error details for failed steps
52+
elif step.status == StepStatus.FAIL and step.error:
53+
step_info["message"] = step.error.split('\n')[0] # First line of error
54+
step_info["error_details"] = cls._extract_error_details(step)
55+
56+
steps.append(step_info)
57+
58+
# Count planned vs completed steps
59+
total_planned = 7 # BOOTSTRAP, LOAD_TEMPLATE, BUILD_TREE, PRE_FLIGHT, GRADE, FEEDBACK, EXPORT
60+
completed = len([s for s in execution.step_results if s.step != StepName.BOOTSTRAP])
61+
62+
return {
63+
"status": execution.status.value if execution.status != PipelineStatus.EMPTY else "unknown",
64+
"failed_at_step": failed_step,
65+
"total_steps_planned": total_planned,
66+
"steps_completed": completed,
67+
"execution_time_ms": execution_time_ms,
68+
"steps": steps
69+
}
70+
71+
@classmethod
72+
def _get_success_message(cls, step: StepResult, execution: PipelineExecution) -> str:
73+
"""Returns success message for a specific pipeline step."""
74+
if step.step == StepName.LOAD_TEMPLATE:
75+
return "Template loaded successfully"
76+
elif step.step == StepName.BUILD_TREE:
77+
return "Criteria tree built successfully"
78+
elif step.step == StepName.PRE_FLIGHT:
79+
return "All preflight checks passed"
80+
elif step.step == StepName.GRADE:
81+
if execution.result and execution.result.final_score is not None:
82+
return f"Grading completed: {execution.result.final_score}/100"
83+
return "Grading completed"
84+
elif step.step == StepName.FEEDBACK:
85+
return "Feedback generated"
86+
elif step.step == StepName.EXPORTER:
87+
return "Results exported"
88+
return ""
89+
90+
@classmethod
91+
def _extract_error_details(cls, step: StepResult) -> Dict[str, Any]:
92+
"""
93+
Extract structured error details from a failed step using its structured error_data.
94+
This provides backward compatibility with the legacy text-based regex parser.
95+
"""
96+
error_details = {}
97+
98+
if step.step == StepName.PRE_FLIGHT and step.error_data:
99+
# error_data is a list of PreflightError objects.
100+
# We map properties from the first critical error for legacy backward compatibility.
101+
if isinstance(step.error_data, list) and len(step.error_data) > 0:
102+
first_error = step.error_data[0]
103+
104+
# Check for Dictionary or Object access depending on how it's passed at runtime
105+
err_type = first_error.type if hasattr(first_error, 'type') else first_error.get('type')
106+
err_details = first_error.details if hasattr(first_error, 'details') else first_error.get('details', {})
107+
108+
if err_type == PreflightCheckType.FILE_CHECK:
109+
error_details["error_type"] = "required_file_missing"
110+
error_details["phase"] = "required_files"
111+
if err_details and "missing_file" in err_details:
112+
error_details["missing_file"] = err_details["missing_file"]
113+
114+
elif err_type == PreflightCheckType.SETUP_COMMAND:
115+
error_details["error_type"] = "setup_command_failed"
116+
error_details["phase"] = "setup_commands"
117+
if err_details:
118+
if "command_name" in err_details:
119+
error_details["command_name"] = err_details["command_name"]
120+
121+
failed_command = {}
122+
if "command" in err_details:
123+
failed_command["command"] = err_details["command"]
124+
if "exit_code" in err_details:
125+
failed_command["exit_code"] = err_details["exit_code"]
126+
if "stderr" in err_details:
127+
failed_command["stderr"] = err_details["stderr"]
128+
if "stdout" in err_details:
129+
failed_command["stdout"] = err_details["stdout"]
130+
131+
# Preserve legacy nested structure
132+
if failed_command:
133+
error_details["failed_command"] = failed_command
134+
135+
return error_details

autograder/steps/pre_flight_step.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def execute(self, pipeline_exec: PipelineExecution) -> PipelineExecution:
7070
data=sandbox, # sandbox is None here, which is correct
7171
status=StepStatus.FAIL,
7272
error=error_msg,
73+
error_data=self._pre_flight_service.fatal_errors,
7374
original_input=pipeline_exec
7475
))
7576

@@ -100,6 +101,7 @@ def execute(self, pipeline_exec: PipelineExecution) -> PipelineExecution:
100101
data=sandbox,#Return Sandbox Here anyway? (How to deal with sandbox destruction)
101102
status=StepStatus.FAIL,
102103
error=error_msg,
104+
error_data=self._pre_flight_service.fatal_errors,
103105
original_input=pipeline_exec
104106
))
105107

docs/architecture/core_structures.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -245,30 +245,31 @@ class PipelineExecution:
245245
start_time: float # Execution start timestamp
246246
```
247247

248-
### Methods
248+
### Pipeline Execution Summary
249249

250-
```python
251-
def get_pipeline_execution_summary() -> dict:
252-
"""
253-
Generate detailed summary for API responses.
254-
255-
Returns:
250+
The `PipelineExecutionSerializer` (`autograder/serializers/pipeline_execution_serializer.py`) handles the conversion of an execution into a dictionary for API responses.
251+
252+
#### `PipelineExecutionSerializer.serialize(execution) -> dict`
253+
254+
Generates detailed summary for API responses.
255+
256+
**Returns:**
257+
```json
258+
{
259+
"status": "success" | "failed",
260+
"failed_at_step": str | None,
261+
"total_steps_planned": int,
262+
"steps_completed": int,
263+
"execution_time_ms": int,
264+
"steps": [
256265
{
257-
"status": "success" | "failed",
258-
"failed_at_step": str | None,
259-
"total_steps_planned": int,
260-
"steps_completed": int,
261-
"execution_time_ms": int,
262-
"steps": [
263-
{
264-
"name": "PRE_FLIGHT",
265-
"status": "success" | "fail",
266-
"message": str,
267-
"error_details": dict | None # Present if step failed
268-
}
269-
]
266+
"name": "PreFlightStep",
267+
"status": "success" | "fail",
268+
"message": str,
269+
"error_details": dict | None # Present if step failed
270270
}
271-
"""
271+
]
272+
}
272273
```
273274

274275
### Example Summary (Success)

docs/guides/github_module.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ In short, the adopted solution consists of running a Docker container with the d
1717
1. [Action.yml](../../action.yml)
1818
Defines the inputs received by the action, describes the output as a JSON evaluation, and runs the container with the received data as environment variables.
1919

20-
2. [Dockerfile](../../github_action/Dockerfile.actions)
20+
2. [Dockerfile](../../Dockerfile.actions)
2121
Builds the container that installs the Autograder repository and sets [Entrypoint.sh](../../github_action/entrypoint.sh) as the script to be executed when the container starts.
2222

2323
3. [Entrypoint.sh](../../github_action/entrypoint.sh)

0 commit comments

Comments
 (0)