|
| 1 | +## CheckAction |
| 2 | + |
| 3 | +This document explains how `org.hyperskill.academy.learning.actions.CheckAction` works, what code paths it executes, and how local and remote checking are combined. |
| 4 | + |
| 5 | +### Entry point |
| 6 | + |
| 7 | +The action is registered in `intellij-plugin/hs-core/resources/META-INF/hs-core.xml`: |
| 8 | + |
| 9 | +```xml |
| 10 | +<action id="HyperskillEducational.Check" class="org.hyperskill.academy.learning.actions.CheckAction"/> |
| 11 | +``` |
| 12 | + |
| 13 | +The implementation is in `intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/CheckAction.kt`. |
| 14 | + |
| 15 | +### High-level flow |
| 16 | + |
| 17 | +`CheckAction.actionPerformed(...)` does the following: |
| 18 | + |
| 19 | +1. Retrieves `Project` from the action event. |
| 20 | +2. Refuses to run while indexing is active (`DumbService.isDumb(project)`). |
| 21 | +3. Clears the check details view. |
| 22 | +4. Saves all open documents. |
| 23 | +5. Reads the current task from `TaskToolWindowView`. |
| 24 | +6. Acquires a per-project lock so only one check can run at a time. |
| 25 | +7. Calls all registered `CheckListener.beforeCheck(...)`. |
| 26 | +8. Starts a background task `StudyCheckTask`. |
| 27 | + |
| 28 | +Relevant code: |
| 29 | + |
| 30 | +- `CheckAction.kt`, `actionPerformed` |
| 31 | +- `CheckAction.kt`, `CheckActionState` |
| 32 | + |
| 33 | +### Core execution graph |
| 34 | + |
| 35 | +```text |
| 36 | +CheckAction.actionPerformed |
| 37 | + -> CheckListener.beforeCheck(project, task) |
| 38 | + -> StudyCheckTask.run(indicator) |
| 39 | + -> localCheck(indicator) |
| 40 | + -> recreateTestFiles(taskDir) |
| 41 | + -> maybe createTests(invisibleTestFiles) |
| 42 | + -> checker.check(indicator) |
| 43 | + -> if local result is Failed: stop |
| 44 | + -> remoteCheckerForTask(project, task) |
| 45 | + -> remoteChecker?.check(project, task, indicator) ?: localResult |
| 46 | + -> onSuccess() |
| 47 | + -> update task.status |
| 48 | + -> update task.feedback |
| 49 | + -> saveItem(task) |
| 50 | + -> checker.onTaskSolved() / checker.onTaskFailed() |
| 51 | + -> TaskToolWindowView.checkFinished(...) |
| 52 | + -> CheckListener.afterCheck(project, task, result) |
| 53 | +``` |
| 54 | + |
| 55 | +### How the local checker is chosen |
| 56 | + |
| 57 | +`StudyCheckTask` creates a local checker through the course configurator: |
| 58 | + |
| 59 | +```kotlin |
| 60 | +val configurator = task.course.configurator |
| 61 | +checker = configurator?.taskCheckerProvider?.getTaskChecker(task, project) |
| 62 | +``` |
| 63 | + |
| 64 | +The generic selection logic is in `intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/TaskCheckerProvider.kt`. |
| 65 | + |
| 66 | +Important behavior: |
| 67 | + |
| 68 | +- `RemoteEduTask` -> no local checker |
| 69 | +- `CodeTask` -> no local checker |
| 70 | +- `TheoryTask` -> no local checker |
| 71 | +- `UnsupportedTask` -> no local checker |
| 72 | +- `EduTask` -> configurator-specific local checker |
| 73 | +- `OutputTask` -> `OutputTaskChecker` |
| 74 | +- `IdeTask` -> `IdeTaskChecker` |
| 75 | + |
| 76 | +This is why not every task goes through local tests. |
| 77 | + |
| 78 | +### What `localCheck(...)` does |
| 79 | + |
| 80 | +`StudyCheckTask.localCheck(...)` performs the local phase: |
| 81 | + |
| 82 | +1. If no checker exists, returns `CheckResult.NO_LOCAL_CHECK`. |
| 83 | +2. Resolves the task directory. |
| 84 | +3. Recreates test files from task metadata before running checks. |
| 85 | +4. For non-Hyperskill courses, restores invisible test files into the project tree. |
| 86 | +5. Calls `checker.check(indicator)`. |
| 87 | + |
| 88 | +Relevant methods: |
| 89 | + |
| 90 | +- `CheckAction.kt`, `localCheck` |
| 91 | +- `CheckAction.kt`, `recreateTestFiles` |
| 92 | +- `CheckAction.kt`, `createTests` |
| 93 | + |
| 94 | +### Why test files are recreated |
| 95 | + |
| 96 | +Before running a local check, the plugin restores author-provided test files from the task model. This prevents a learner from changing, deleting, or corrupting tests to fake a successful check. |
| 97 | + |
| 98 | +For framework lessons, the plugin uses `FrameworkLessonManager` cached original test files rather than the current `task.taskFiles`, because framework tasks may otherwise carry stale test data from another stage. |
| 99 | + |
| 100 | +### How local test execution works |
| 101 | + |
| 102 | +The standard base implementation for local `EduTask` checking is `EduTaskCheckerBase` in: |
| 103 | + |
| 104 | +`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/EduTaskCheckerBase.kt` |
| 105 | + |
| 106 | +Its `check(...)` method: |
| 107 | + |
| 108 | +1. Hides the Run tool window. |
| 109 | +2. Calls `EnvironmentChecker.getEnvironmentError(project, task)`. |
| 110 | +3. Builds run configurations. |
| 111 | +4. Validates each configuration. |
| 112 | +5. Executes them using IntelliJ run infrastructure. |
| 113 | +6. Collects test results. |
| 114 | +7. Produces a `CheckResult`. |
| 115 | + |
| 116 | +If execution starts but tests do not actually run, subclasses may translate stderr into a more specific error result. |
| 117 | + |
| 118 | +### How run configurations are built and executed |
| 119 | + |
| 120 | +The utility layer is in: |
| 121 | + |
| 122 | +`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/CheckUtils.kt` |
| 123 | + |
| 124 | +Key pieces: |
| 125 | + |
| 126 | +- `getCustomRunConfigurationForChecker(...)` |
| 127 | +- `createDefaultRunConfiguration(...)` |
| 128 | +- `executeRunConfigurations(...)` |
| 129 | + |
| 130 | +Execution details: |
| 131 | + |
| 132 | +- A custom task-specific run configuration in `.idea/runConfigurations` is preferred if present. |
| 133 | +- Otherwise, IntelliJ derives temporary run configurations from PSI context. |
| 134 | +- Configurations run through `ProgramRunner` and `ExecutionEnvironmentBuilder`. |
| 135 | +- The plugin tracks all started environments and waits on a `CountDownLatch`. |
| 136 | +- Test results are collected through a `TestResultCollector`. |
| 137 | + |
| 138 | +### Local-vs-remote ordering |
| 139 | + |
| 140 | +`StudyCheckTask.run(...)` always does the local phase first: |
| 141 | + |
| 142 | +```kotlin |
| 143 | +val localCheckResult = localCheck(indicator) |
| 144 | +if (localCheckResult.status === CheckStatus.Failed) { |
| 145 | + result = localCheckResult |
| 146 | + return |
| 147 | +} |
| 148 | +val remoteChecker = remoteCheckerForTask(project, task) |
| 149 | +result = remoteChecker?.check(project, task, indicator) ?: localCheckResult |
| 150 | +``` |
| 151 | + |
| 152 | +This means: |
| 153 | + |
| 154 | +- A local `Failed` result stops the pipeline immediately. |
| 155 | +- Remote checking is attempted only if local checking did not fail. |
| 156 | +- If there is no remote checker, the final result is the local result. |
| 157 | + |
| 158 | +### How the remote checker is chosen |
| 159 | + |
| 160 | +Remote checkers are selected through the extension point: |
| 161 | + |
| 162 | +`HyperskillEducational.remoteTaskChecker` |
| 163 | + |
| 164 | +The manager is: |
| 165 | + |
| 166 | +`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/remote/RemoteTaskCheckerManager.kt` |
| 167 | + |
| 168 | +It: |
| 169 | + |
| 170 | +1. Collects all registered remote checkers. |
| 171 | +2. Filters them by `canCheck(project, task)`. |
| 172 | +3. Returns exactly one checker or `null`. |
| 173 | +4. Throws if more than one checker matches. |
| 174 | + |
| 175 | +### Hyperskill remote checker |
| 176 | + |
| 177 | +Hyperskill registers: |
| 178 | + |
| 179 | +```xml |
| 180 | +<remoteTaskChecker implementation="org.hyperskill.academy.learning.stepik.hyperskill.checker.HyperskillRemoteTaskChecker"/> |
| 181 | +``` |
| 182 | + |
| 183 | +Implementation: |
| 184 | + |
| 185 | +`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillRemoteTaskChecker.kt` |
| 186 | + |
| 187 | +It can check only when: |
| 188 | + |
| 189 | +- `task.course is HyperskillCourse` |
| 190 | +- `HyperskillCheckConnector.isRemotelyChecked(task)` is true |
| 191 | + |
| 192 | +That remote set is: |
| 193 | + |
| 194 | +- `CodeTask` |
| 195 | +- `RemoteEduTask` |
| 196 | +- `UnsupportedTask` |
| 197 | + |
| 198 | +### Hyperskill remote task behavior by type |
| 199 | + |
| 200 | +#### CodeTask |
| 201 | + |
| 202 | +Path: |
| 203 | + |
| 204 | +- `HyperskillRemoteTaskChecker.check(...)` |
| 205 | +- `HyperskillCheckConnector.checkCodeTask(...)` |
| 206 | + |
| 207 | +Behavior: |
| 208 | + |
| 209 | +1. Validates that task id exists. |
| 210 | +2. Tries websocket-based check session. |
| 211 | +3. If websocket path fails, falls back to HTTP submission. |
| 212 | +4. Polls the submission until status changes from `evaluation`. |
| 213 | + |
| 214 | +#### RemoteEduTask |
| 215 | + |
| 216 | +Path: |
| 217 | + |
| 218 | +- `HyperskillRemoteTaskChecker.check(...)` |
| 219 | +- `HyperskillCheckConnector.checkRemoteEduTask(...)` |
| 220 | + |
| 221 | +Behavior: |
| 222 | + |
| 223 | +1. Validates that task id exists. |
| 224 | +2. Collects solution files. |
| 225 | +3. Creates an attempt. |
| 226 | +4. Creates and posts a submission. |
| 227 | +5. Polls until final status is received. |
| 228 | + |
| 229 | +#### UnsupportedTask |
| 230 | + |
| 231 | +Path: |
| 232 | + |
| 233 | +- `HyperskillRemoteTaskChecker.check(...)` |
| 234 | +- `HyperskillCheckConnector.checkUnsupportedTask(...)` |
| 235 | + |
| 236 | +Behavior: |
| 237 | + |
| 238 | +1. Does not create a new submission. |
| 239 | +2. Loads existing submissions from the platform. |
| 240 | +3. Derives solved/failed state from the latest known submissions. |
| 241 | + |
| 242 | +### Hyperskill local EduTask behavior |
| 243 | + |
| 244 | +Hyperskill `EduTask` is special: |
| 245 | + |
| 246 | +- It is checked locally. |
| 247 | +- It does not use the remote checker. |
| 248 | +- After the local result is finalized, `HyperskillCheckListener.afterCheck(...)` may asynchronously post the solution to Hyperskill. |
| 249 | + |
| 250 | +This listener is registered in `META-INF/Hyperskill.xml`. |
| 251 | + |
| 252 | +Implementation: |
| 253 | + |
| 254 | +`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillCheckListener.kt` |
| 255 | + |
| 256 | +Behavior: |
| 257 | + |
| 258 | +- `beforeCheck(...)` updates Hyperskill metrics. |
| 259 | +- `afterCheck(...)` restarts metrics for unsolved current tasks. |
| 260 | +- For non-remote, non-theory Hyperskill tasks, it posts the local solution to Hyperskill in background if the user is logged in. |
| 261 | + |
| 262 | +This postback is not the same thing as remote checking. It is a follow-up side effect after local checking has already completed. |
| 263 | + |
| 264 | +### Result finalization |
| 265 | + |
| 266 | +When background execution succeeds, `StudyCheckTask.onSuccess()`: |
| 267 | + |
| 268 | +1. Stores `task.status = checkResult.status` |
| 269 | +2. Stores `task.feedback` |
| 270 | +3. Persists the task via `saveItem(task)` |
| 271 | +4. Calls checker lifecycle hooks |
| 272 | +5. Updates the tool window |
| 273 | +6. Refreshes course progress and project view |
| 274 | +7. Calls all `CheckListener.afterCheck(...)` |
| 275 | + |
| 276 | +### Error and cancel behavior |
| 277 | + |
| 278 | +If the background task is cancelled: |
| 279 | + |
| 280 | +- the tool window is reset to `readyToCheck()` |
| 281 | + |
| 282 | +If an exception happens: |
| 283 | + |
| 284 | +- refresh-token failure is converted to `failedToSubmit(...)` |
| 285 | +- everything else becomes generic `failedToCheck` |
| 286 | + |
| 287 | +### Practical summary |
| 288 | + |
| 289 | +For the common Hyperskill task classes: |
| 290 | + |
| 291 | +- `EduTask`: local tests first, then optional async postback to Hyperskill |
| 292 | +- `RemoteEduTask`: remote-only check |
| 293 | +- `CodeTask`: remote-only check |
| 294 | +- `UnsupportedTask`: remote state lookup only, no fresh submission |
| 295 | + |
| 296 | +This local-first and then maybe-remote pattern is the most important rule to keep in mind when debugging `CheckAction`. |
0 commit comments