diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillLanguages.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillLanguages.kt index 634ffc742..d1a2f20e3 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillLanguages.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillLanguages.kt @@ -36,7 +36,7 @@ enum class HyperskillLanguages(private val id: String, private val languageName: /** * Request language is language plugin received from JBA in requests - * @see [org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepWithProjectRequest] + * @see [org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepRequest] */ open val requestLanguage: String = languageName diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillRestService.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillRestService.kt index e58fd1d9d..3ec268b58 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillRestService.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillRestService.kt @@ -154,14 +154,10 @@ open class HyperskillRestService : OAuthRestService(HYPERSKILL) { if (!HyperskillConnector.getInstance().isLoggedIn()) { error("Attempt to open step for unauthorized user") } - val projectId = getSelectedProjectIdUnderProgress() ?: return openInIDE( - HyperskillOpenStepRequest( - stepId, - language, - isLanguageSelectedByUser - ), request, context - ) - return openInIDE(HyperskillOpenStepWithProjectRequest(projectId, stepId, language, isLanguageSelectedByUser), request, context) + // Problems are always opened in a separate problems project, so the project selected + // on Hyperskill is irrelevant here and must not be requested: an extra API call slows + // opening down and its failure would block opening the problem + return openInIDE(HyperskillOpenStepRequest(stepId, language, isLanguageSelectedByUser), request, context) } private fun getLanguageSelectedByUser(): Result { diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillUtils.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillUtils.kt index d0bdc73c8..58f71f2b6 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillUtils.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillUtils.kt @@ -28,7 +28,7 @@ import org.hyperskill.academy.learning.stepik.hyperskill.api.HyperskillConnector import org.hyperskill.academy.learning.stepik.hyperskill.api.HyperskillStepSource import org.hyperskill.academy.learning.stepik.hyperskill.api.WithPaginationMetaData import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenInIdeRequestHandler -import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepWithProjectRequest +import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepRequest import org.hyperskill.academy.learning.stepik.hyperskill.settings.HyperskillSettings import org.hyperskill.academy.learning.yaml.YamlFormatSynchronizer import javax.swing.event.HyperlinkEvent @@ -220,7 +220,7 @@ private fun openStep(project: Project, task: Task?, nextActivityInfo: NextActivi val language = HyperskillLanguages.getRequestLanguage(course.languageId) ?: return ProjectOpener.getInstance().open( HyperskillOpenInIdeRequestHandler, - HyperskillOpenStepWithProjectRequest(course.id, nextStep.id, language) + HyperskillOpenStepRequest(nextStep.id, language) ).onError { logger().warn("Opening the next activity resulted in an error: ${it.message}. The error was ignored and not displayed for the user.") } diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/courseGeneration/HyperskillOpenInIdeRequestHandler.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/courseGeneration/HyperskillOpenInIdeRequestHandler.kt index 621c855dc..11934d5f4 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/courseGeneration/HyperskillOpenInIdeRequestHandler.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/courseGeneration/HyperskillOpenInIdeRequestHandler.kt @@ -11,7 +11,6 @@ import org.hyperskill.academy.learning.* import org.hyperskill.academy.learning.authUtils.requestFocus import org.hyperskill.academy.learning.courseFormat.Course import org.hyperskill.academy.learning.courseFormat.EduFormatNames.HYPERSKILL_PROJECTS_URL -import org.hyperskill.academy.learning.courseFormat.EduFormatNames.KOTLIN import org.hyperskill.academy.learning.courseFormat.FrameworkLesson import org.hyperskill.academy.learning.courseFormat.Lesson import org.hyperskill.academy.learning.courseFormat.Section @@ -116,15 +115,27 @@ object HyperskillOpenInIdeRequestHandler : OpenInIdeRequestHandler Boolean) -> Pair? ): Project? { when (request) { - is HyperskillOpenStepRequestBase -> { + is HyperskillOpenStepRequest -> { val stepId = request.stepId - val stepSource = getStepSource(stepId, request.isLanguageSelectedByUser) - val isAndroidEnvRequired = stepSource.framework == EduNames.ANDROID - val courseFilter: (Course) -> Boolean = if (isAndroidEnvRequired) ::hasAndroidEnvironment else { _ -> true } - val (project, course) = findExistingProject(findProject, request, courseFilter) ?: return null - val hyperskillCourse = course as HyperskillCourse - hyperskillCourse.addProblemsWithTopicWithFiles(project, stepSource) - hyperskillCourse.selectedProblem = stepId + // Opening a problem must run in the background, not on the EDT. The post-solve "next activity" flow invokes + // this path directly on the UI thread, where two things go wrong: + // 1. `getStepSource` performs a blocking network request, tripping the "Network requests from EDT are not + // allowed" assertion and freezing the IDE; + // 2. `addProblemsWithTopicWithFiles` shows a modal progress that pumps the event queue, so a YAML reload + // queued by the produced file changes can run against a not-yet-complete course model and report + // "parent for '' was not found" when the user checks the problem. + // Running the whole step-opening off the EDT keeps the course model consistent before any reload. + val (project, hyperskillCourse) = computeUnderProgress(title = EduCoreBundle.message("hyperskill.loading.problems")) { + val stepSource = getStepSource(stepId, request.isLanguageSelectedByUser) + val isAndroidEnvRequired = stepSource.framework == EduNames.ANDROID + val courseFilter: (Course) -> Boolean = if (isAndroidEnvRequired) ::hasAndroidEnvironment else { _ -> true } + val (project, course) = findExistingProject(findProject, request, courseFilter) ?: return@computeUnderProgress null + val hyperskillCourse = course as HyperskillCourse + hyperskillCourse.addProblemsWithTopicWithFiles(project, stepSource) + hyperskillCourse.selectedProblem = stepId + project to hyperskillCourse + } ?: return null + runInEdt { requestFocus() navigateToStep(project, hyperskillCourse, stepId) @@ -211,21 +222,10 @@ object HyperskillOpenInIdeRequestHandler : OpenInIdeRequestHandler? { return when (request) { is HyperskillOpenProjectStageRequest -> findProject { it.matchesById(request.projectId) } - is HyperskillOpenStepWithProjectRequest -> { - val hyperskillLanguage = request.language - val (languageId, languageVersion) = HyperskillLanguages.getLanguageIdAndVersion(hyperskillLanguage) ?: return null - - findProject { - it.matchesById(request.projectId) && it.languageId == languageId && it.languageVersion == languageVersion && courseFilter(it) - } - ?: findProject { course -> course.isHyperskillProblemsCourse(hyperskillLanguage) } - } - is HyperskillOpenStepRequest -> { val hyperskillLanguage = request.language - val (languageId, languageVersion) = HyperskillLanguages.getLanguageIdAndVersion(hyperskillLanguage) ?: return null - findProject { it is HyperskillCourse && it.languageId == languageId && it.languageVersion == languageVersion && courseFilter(it) } - ?: findProject { course -> course.isHyperskillProblemsCourse(hyperskillLanguage) && courseFilter(course) } + HyperskillLanguages.getLanguageIdAndVersion(hyperskillLanguage) ?: return null + findProject { course -> course.isHyperskillProblemsCourse(hyperskillLanguage) && courseFilter(course) } } } } @@ -241,7 +241,8 @@ object HyperskillOpenInIdeRequestHandler : OpenInIdeRequestHandler { val (languageId, languageVersion) = HyperskillLanguages.getLanguageIdAndVersion(hyperskillLanguage) ?: return Err( @@ -253,41 +254,26 @@ object HyperskillOpenInIdeRequestHandler : OpenInIdeRequestHandler { + LOG.info("Processing HyperskillOpenStepRequest: stepId=${request.stepId}, language=${request.language}") + indicator.text2 = EduCoreBundle.message("hyperskill.loading.step.info") + indicator.fraction = 0.2 - request as HyperskillOpenWithProjectRequestBase - LOG.info("Fetching project info for projectId=${request.projectId}") - indicator.text = EduCoreBundle.message("hyperskill.loading.project.info") - indicator.text2 = EduCoreBundle.message("hyperskill.loading.project.info.details") - indicator.fraction = 0.1 + val newProject = HyperskillProject() + val hyperskillLanguage = request.language + val stepSource = getStepSource(request.stepId, request.isLanguageSelectedByUser) + val hyperskillCourse = createHyperskillCourse(request, hyperskillLanguage, newProject, stepSource).onError { return Err(it) } - val projectStartTime = System.currentTimeMillis() - val hyperskillProject = HyperskillConnector.getInstance().getProject(request.projectId).onError { - LOG.warn("Failed to fetch project ${request.projectId}: $it") - return Err(ValidationErrorMessage(it)) - } - LOG.info("Project info fetched in ${System.currentTimeMillis() - projectStartTime}ms") + hyperskillCourse.validateLanguage(hyperskillLanguage).onError { + LOG.warn("Language validation failed: $it") + return Err(it) + } - indicator.fraction = 0.2 - indicator.text = EduCoreBundle.message("hyperskill.creating.course") - indicator.text2 = EduCoreBundle.message("hyperskill.creating.course.details") + indicator.fraction = 0.5 + indicator.text2 = EduCoreBundle.message("hyperskill.loading.problems") + hyperskillCourse.addProblemsWithTopicWithFiles(null, stepSource) + hyperskillCourse.selectedProblem = request.stepId - val hyperskillLanguage = if (request is HyperskillOpenStepWithProjectRequest) request.language else hyperskillProject.language - LOG.info("Creating course for language: $hyperskillLanguage") + indicator.fraction = 1.0 + LOG.info("HyperskillOpenStepRequest processed in ${System.currentTimeMillis() - totalStartTime}ms") + Ok(hyperskillCourse) + } - val hyperskillCourse = createHyperskillCourse(request, hyperskillLanguage, hyperskillProject).onError { - LOG.warn("Failed to create course: $it") - return Err(it) - } + is HyperskillOpenProjectStageRequest -> { + LOG.info("Fetching project info for projectId=${request.projectId}") + indicator.text = EduCoreBundle.message("hyperskill.loading.project.info") + indicator.text2 = EduCoreBundle.message("hyperskill.loading.project.info.details") + indicator.fraction = 0.1 + + val projectStartTime = System.currentTimeMillis() + val hyperskillProject = HyperskillConnector.getInstance().getProject(request.projectId).onError { + LOG.warn("Failed to fetch project ${request.projectId}: $it") + return Err(ValidationErrorMessage(it)) + } + LOG.info("Project info fetched in ${System.currentTimeMillis() - projectStartTime}ms") - hyperskillCourse.validateLanguage(hyperskillLanguage).onError { - LOG.warn("Language validation failed: $it") - return Err(it) - } + indicator.fraction = 0.2 + indicator.text = EduCoreBundle.message("hyperskill.creating.course") + indicator.text2 = EduCoreBundle.message("hyperskill.creating.course.details") - indicator.fraction = 0.4 + val hyperskillLanguage = hyperskillProject.language + LOG.info("Creating course for language: $hyperskillLanguage") - when (request) { - is HyperskillOpenStepWithProjectRequest -> { - LOG.info("Loading problems for stepId=${request.stepId}") - indicator.text = EduCoreBundle.message("hyperskill.loading.problems") - indicator.text2 = EduCoreBundle.message("hyperskill.loading.problems.details") - hyperskillCourse.addProblemsWithTopicWithFiles(null, getStepSource(request.stepId, request.isLanguageSelectedByUser)) - hyperskillCourse.selectedProblem = request.stepId - } + val hyperskillCourse = createHyperskillCourse(request, hyperskillLanguage, hyperskillProject).onError { + LOG.warn("Failed to create course: $it") + return Err(it) + } + + hyperskillCourse.validateLanguage(hyperskillLanguage).onError { + LOG.warn("Language validation failed: $it") + return Err(it) + } + + indicator.fraction = 0.4 - is HyperskillOpenProjectStageRequest -> { LOG.info("Loading stages for new project, stageId=${request.stageId}") indicator.text = EduCoreBundle.message("hyperskill.loading.stages") indicator.text2 = EduCoreBundle.message("hyperskill.loading.stages.details") @@ -376,12 +359,12 @@ object HyperskillOpenInIdeRequestHandler : OpenInIdeRequestHandler' was not found" error reported on Check. + // Captures which item/dir failed to resolve, the live course structure, and the stack trace of whatever + // triggered this reload. Remove once the root cause is confirmed. + logParentNotFoundDiagnostics(project, parentDir, course, customContentPath) + } return itemContainer ?: loadingError(EduCoreBundle.message("yaml.editor.invalid.format.parent.not.found", name)) } + private fun StudyItem.logParentNotFoundDiagnostics( + project: Project, + parentDir: VirtualFile, + course: Course, + customContentPath: String + ) { + val sectionDir = parentDir.parent + val structure = course.sections.joinToString("; ") { section -> + "section '${section.name}'(presentable='${section.presentableName}') -> [${section.lessons.joinToString(", ") { it.name }}]" + } + LOG.warn( + "ALT-11025 parent not found while reloading YAML:" + + " item='$name' type=${this::class.simpleName}" + + " parentDir='${parentDir.path}' (name='${parentDir.name}')" + + " sectionDir='${sectionDir?.path}' (name='${sectionDir?.name}')" + + " course=${course::class.simpleName} name='${course.name}' customContentPath='$customContentPath'" + + " courseDirByCustomPath='${project.courseDir.findFileByRelativePath(customContentPath)?.path}'" + + " parentDir.getLesson='${parentDir.getLesson(project)?.name}'" + + " sectionDir.getSection='${sectionDir?.getSection(project)?.name}'" + + " course.topLevelLessons=[${course.lessons.joinToString(", ") { it.name }}]" + + " course.sectionsWithLessons=[$structure]", + Throwable("loadItem trigger stack (ALT-11025 diagnostics)") + ) + } + private fun T.applyChanges(project: Project, deserializedItem: T) { getChangeApplierForItem(project, deserializedItem).applyChanges(this, deserializedItem) } diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/MockProjectOpener.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/MockProjectOpener.kt index 0f305566f..8493927d0 100644 --- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/MockProjectOpener.kt +++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/MockProjectOpener.kt @@ -7,9 +7,23 @@ import org.hyperskill.academy.learning.courseGeneration.ProjectOpener class MockProjectOpener : ProjectOpener() { var project: Project? = null + set(value) { + field = value + courseOpenedInNewProject = null + } + + /** + * The course passed to [newProject] during the last `open` call. + * Unlike production, the mock generates course files into the current test project + * and never replaces `project.course`, so this is the only way to inspect + * a course "opened in a new project". + */ + var courseOpenedInNewProject: Course? = null + private set override fun newProject(course: Course): Boolean { assertProject() + courseOpenedInNewProject = course course.configurator?.beforeCourseStarted(course) course.createCourseFiles(project!!) return true diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillNextActivityTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillNextActivityTest.kt index 8054998a0..8a75b9b91 100644 --- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillNextActivityTest.kt +++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillNextActivityTest.kt @@ -1,6 +1,7 @@ package org.hyperskill.academy.learning.stepik.hyperskill import org.hyperskill.academy.learning.* +import org.hyperskill.academy.learning.configurators.FakeGradleBasedLanguage import org.hyperskill.academy.learning.courseFormat.EduFormatNames.HYPERSKILL_TOPICS import org.hyperskill.academy.learning.courseFormat.tasks.CodeTask import org.hyperskill.academy.learning.courseFormat.tasks.Task @@ -147,7 +148,9 @@ class HyperskillNextActivityTest : EduTestCase() { } private fun createHyperskillProblemsProject() { - hyperskillCourseWithFiles { + // The next step can only be opened in the already open project if it is a problems project, + // which is identified by its name + hyperskillCourseWithFiles(name = getProblemsProjectName(FakeGradleBasedLanguage.id)) { section(HYPERSKILL_TOPICS) { lesson(theoryStepFromInitialProject.title) { theoryTask("Theory", stepId = theoryStepFromInitialProject.id) { diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillProblemLoadingTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillProblemLoadingTest.kt index 02b4c5b2e..c90eb6c04 100644 --- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillProblemLoadingTest.kt +++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillProblemLoadingTest.kt @@ -12,7 +12,6 @@ import org.hyperskill.academy.learning.stepik.hyperskill.api.HyperskillConnector import org.hyperskill.academy.learning.stepik.hyperskill.api.MockHyperskillConnector import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenInIdeRequestHandler.addProblemsWithTopicWithFiles import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenInIdeRequestHandler.getStepSource -import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepWithProjectRequest import org.hyperskill.academy.learning.stepik.hyperskill.projectOpen.HyperskillProjectOpenerTestBase.Companion.StepInfo import org.hyperskill.academy.learning.stepik.hyperskill.projectOpen.HyperskillProjectOpenerTestBase.Companion.TopicInfo import org.junit.Test @@ -71,8 +70,7 @@ class HyperskillProblemLoadingTest : EduTestCase() { } as HyperskillCourse course.hyperskillProject = HyperskillProject() course.stages = listOf(HyperskillStage(1, "", 1)) - val request = HyperskillOpenStepWithProjectRequest(1, 4894, "TEXT") - course.addProblemsWithTopicWithFiles(null, getStepSource(request.stepId, request.isLanguageSelectedByUser)) + course.addProblemsWithTopicWithFiles(null, getStepSource(step4894.id, false)) return course } diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/projectOpen/HyperskillProjectOpenTopicProblemsTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/projectOpen/HyperskillProjectOpenTopicProblemsTest.kt index 9ef7480c4..0005857ca 100644 --- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/projectOpen/HyperskillProjectOpenTopicProblemsTest.kt +++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/projectOpen/HyperskillProjectOpenTopicProblemsTest.kt @@ -1,18 +1,22 @@ package org.hyperskill.academy.learning.stepik.hyperskill.projectOpen import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.progress.EmptyProgressIndicator import org.hyperskill.academy.learning.configurators.FakeGradleBasedLanguage +import org.hyperskill.academy.learning.course import org.hyperskill.academy.learning.courseFormat.EduFormatNames.HYPERSKILL_TOPICS import org.hyperskill.academy.learning.courseFormat.ext.CourseValidationResult import org.hyperskill.academy.learning.courseFormat.ext.PluginsRequired +import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillCourse import org.hyperskill.academy.learning.hasParams import org.hyperskill.academy.learning.messages.EduCoreBundle import org.hyperskill.academy.learning.onError import org.hyperskill.academy.learning.pathWithoutPrams +import org.hyperskill.academy.learning.stepik.hyperskill.getProblemsProjectName import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenInIdeRequestHandler import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepRequest -import org.hyperskill.academy.learning.stepik.hyperskill.courseGeneration.HyperskillOpenStepWithProjectRequest import org.hyperskill.academy.learning.stepik.hyperskill.hyperskillCourse +import org.hyperskill.academy.learning.stepik.hyperskill.hyperskillCourseWithFiles import org.hyperskill.academy.learning.stepik.hyperskill.projectOpen.HyperskillProjectOpenerTestBase.Companion.StepInfo import org.hyperskill.academy.learning.stepik.hyperskill.projectOpen.HyperskillProjectOpenerTestBase.Companion.TopicInfo import org.junit.Test @@ -20,6 +24,8 @@ import org.junit.Test class HyperskillProjectOpenTopicProblemsTest : HyperskillProjectOpenerTestBase() { + private val stepSourceRequestCounts = mutableMapOf() + override fun setUp() { super.setUp() configureMockResponsesForStages() @@ -28,20 +34,69 @@ class HyperskillProjectOpenTopicProblemsTest : HyperskillProjectOpenerTestBase() @Test fun `test open non dataset problem with language chosen by user`() { - val request = HyperskillOpenStepWithProjectRequest(1, step10960.id, "TEXT", true) + val request = HyperskillOpenStepRequest(step10960.id, "TEXT", true) assertThrows(IllegalStateException::class.java) { mockProjectOpener.open(HyperskillOpenInIdeRequestHandler, request) } } @Test - fun `test open non dataset problem with language chosen by user without selected project`() { + fun `test open non dataset problem with supported language chosen by user`() { val request = HyperskillOpenStepRequest(step10960.id, FakeGradleBasedLanguage.id, true) assertThrows(IllegalStateException::class.java) { mockProjectOpener.open(HyperskillOpenInIdeRequestHandler, request) } } + @Test + fun `test get course loads step source once`() { + val request = HyperskillOpenStepRequest(step2640.id, FakeGradleBasedLanguage.id) + + val course = HyperskillOpenInIdeRequestHandler.getCourse(request, EmptyProgressIndicator()) + .onError { error("Course should be created: $it") } as HyperskillCourse + + assertProblemLoaded(course, TOPIC_85_NAME, step2640.title) + assertStepSourceRequestedOnce(step2640) + } + + @Test + fun `test open problem reuses problems project`() { + val problemsCourse = hyperskillCourseWithFiles( + projectId = null, + name = getProblemsProjectName(FakeGradleBasedLanguage.id) + ) {} + + val request = HyperskillOpenStepRequest(step2640.id, FakeGradleBasedLanguage.id) + mockProjectOpener.open(HyperskillOpenInIdeRequestHandler, request) + + assertSame(problemsCourse, project.course) + assertProblemLoaded(problemsCourse, TOPIC_85_NAME, step2640.title) + } + + @Test + fun `test open problem does not reuse regular hyperskill project and creates problems project`() { + val regularCourse = hyperskillCourseWithFiles { + frameworkLesson("lesson1") { + eduTask("task1", stepId = 1) { + taskFile("src/Task.kt", "stage 1") + } + } + } + + val request = HyperskillOpenStepRequest(step2640.id, FakeGradleBasedLanguage.id) + mockProjectOpener.open(HyperskillOpenInIdeRequestHandler, request).onError { error("Failed to open the problem: $it") } + + // the regular project must not be reused: the problem goes to a newly created problems project + assertSame(regularCourse, project.course) + assertNull(regularCourse.getTopicsSection()) + + val openedCourse = mockProjectOpener.courseOpenedInNewProject as? HyperskillCourse + ?: error("A new problems project should have been created") + assertEquals(getProblemsProjectName(FakeGradleBasedLanguage.id), openedCourse.name) + assertNull(openedCourse.hyperskillProject) + assertProblemLoaded(openedCourse, TOPIC_85_NAME, step2640.title) + } + @Test fun `test unknown language`() { val unknownLanguage = "Unknown language" @@ -80,7 +135,7 @@ class HyperskillProjectOpenTopicProblemsTest : HyperskillProjectOpenerTestBase() } }) - mockProjectOpener.open(HyperskillOpenInIdeRequestHandler, HyperskillOpenStepWithProjectRequest(1, 4, language)).onError { + mockProjectOpener.open(HyperskillOpenInIdeRequestHandler, HyperskillOpenStepRequest(4, language)).onError { checkError(it) return } @@ -88,10 +143,22 @@ class HyperskillProjectOpenTopicProblemsTest : HyperskillProjectOpenerTestBase() error("Error is expected: project shouldn't open") } + private fun assertProblemLoaded(course: HyperskillCourse, topicName: String, problemName: String) { + val task = course.getTopicsSection()?.getLesson(topicName)?.getTask(problemName) + assertNotNull("Failed to find `$problemName` problem in `$topicName` topic", task) + } + + private fun assertStepSourceRequestedOnce(stepInfo: StepInfo) { + assertEquals("Step source should be loaded once", 1, stepSourceRequestCounts[stepInfo.id] ?: 0) + } + private fun configureMockResponsesForProblems() { requestedInformation.forEach { information -> mockConnector.withResponseHandler(testRootDisposable) { request, _ -> if (request.pathWithoutPrams.endsWith(information.path) && request.hasParams(information.param)) { + if (information is StepInfo) { + stepSourceRequestCounts[information.id] = (stepSourceRequestCounts[information.id] ?: 0) + 1 + } mockResponse(information.file) } else null @@ -101,6 +168,7 @@ class HyperskillProjectOpenTopicProblemsTest : HyperskillProjectOpenerTestBase() companion object { private const val TOPIC_NAME = "topicName" + private const val TOPIC_85_NAME = "Wildcards" private val step2640 = StepInfo(2640, "Packing bakeries") private val step2641 = StepInfo(2641, "List multiplicator") @@ -127,4 +195,4 @@ class HyperskillProjectOpenTopicProblemsTest : HyperskillProjectOpenerTestBase() step12164, topic1034 ) } -} \ No newline at end of file +} diff --git a/intellij-plugin/hs-features/ai-debugger-core/build.gradle.kts b/intellij-plugin/hs-features/ai-debugger-core/build.gradle.kts index 07e81447e..0e7b6ddd4 100644 --- a/intellij-plugin/hs-features/ai-debugger-core/build.gradle.kts +++ b/intellij-plugin/hs-features/ai-debugger-core/build.gradle.kts @@ -18,10 +18,14 @@ dependencies { api(libs.educational.ml.library.core) { excludeKotlinDeps() exclude(group = "net.java.dev.jna") + // IntelliJ Platform bundles its own (newer) fastutil; an older transitive one + // shadows it on the test classpath and breaks light test project initialization + exclude(group = "it.unimi.dsi") } api(libs.educational.ml.library.debugger) { excludeKotlinDeps() exclude(group = "net.java.dev.jna") + exclude(group = "it.unimi.dsi") } compileOnly(libs.kotlinx.serialization) {