Repository: intellij-rust/intellij-rust
Issue: Tests are executed in the wrong working directory when debugging

## Environment

* **IntelliJ Rust plugin version:** 0.2.122.3105-201
* **Rust toolchain version:** Nightly
* **IDE name and version:** CL-201.7223.86
* **Operating system:** Arch linux

## Problem description

When debugging, tests appear to always run in the working directory of the workspace instead of the working directory of the concrete cargo project. Tests that reference relative paths will fail.

## Steps to reproduce

https://github.com/mahkoh/irtestcase

Works:

```
cargo test --workspace
```

Fails:

Try to debug the test in CLion

Changed files:
src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt, src/main/kotlin/org/rust/cargo/runconfig/buildtool/RsBuildEventsConverter.kt, src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt, src/main/kotlin/org/rust/cargo/runconfig/RsExecutableRunner.kt, src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt, src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt, src/main/kotlin/org/rust/cargo/project/model/impl/CargoProjectImpl.kt, src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildContext.kt, src/main/kotlin/org/rust/lang/core/resolve/NameResolution.kt, toml/src/main/kotlin/org/rust/toml/resolve/CargoDependencyReferenceProvider.kt

diff --git a/src/main/kotlin/org/rust/cargo/project/model/impl/CargoProjectImpl.kt b/src/main/kotlin/org/rust/cargo/project/model/impl/CargoProjectImpl.kt
index 83d2a26a4..86ae0fe5b 100644
--- a/src/main/kotlin/org/rust/cargo/project/model/impl/CargoProjectImpl.kt
+++ b/src/main/kotlin/org/rust/cargo/project/model/impl/CargoProjectImpl.kt
@@ -483,9 +483,9 @@ data class CargoProjectImpl(
         // "rustc" package was renamed to "rustc_middle" in https://github.com/rust-lang/rust/pull/70536
         // so starting with rustc 1.42 a stable way to identify it is to try to find any of some possible packages
         val possiblePackages = listOf("rustc", "rustc_middle", "rustc_typeck")
-        return workspace.findPackage(AutoInjectedCrates.STD) != null &&
-            workspace.findPackage(AutoInjectedCrates.CORE) != null &&
-            possiblePackages.any { workspace.findPackage(it) != null }
+        return workspace.findPackageByName(AutoInjectedCrates.STD) != null &&
+            workspace.findPackageByName(AutoInjectedCrates.CORE) != null &&
+            possiblePackages.any { workspace.findPackageByName(it) != null }
     }
 
     fun withStdlib(result: TaskResult<StandardLibrary>): CargoProjectImpl = when (result) {
diff --git a/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt b/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt
index ab2e7e74f..9ae3129dd 100644
--- a/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt
+++ b/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt
@@ -47,7 +47,8 @@ interface CargoWorkspace {
 
     val featureGraph: FeatureGraph
 
-    fun findPackage(name: String): Package? = packages.find { it.name == name || it.normName == name }
+    fun findPackageById(id: PackageId): Package? = packages.find { it.id == id }
+    fun findPackageByName(name: String): Package? = packages.find { it.name == name || it.normName == name }
 
     fun findTargetByCrateRoot(root: VirtualFile): Target?
     fun isCrateRoot(root: VirtualFile) = findTargetByCrateRoot(root) != null
@@ -67,6 +68,7 @@ interface CargoWorkspace {
         val contentRoot: VirtualFile?
         val rootDirectory: Path
 
+        val id: String
         val name: String
         val normName: String get() = name.replace('-', '_')
 
@@ -157,10 +159,13 @@ interface CargoWorkspace {
         Unclassified(null),
 
         Stdlib("stdlib?"),
+
         // [dependencies]
         Normal(null),
+
         // [dev-dependencies]
         Development("dev"),
+
         // [build-dependencies]
         Build("build")
     }
@@ -288,7 +293,9 @@ private class WorkspaceImpl(
             val stdCratePackageRoots = stdlib.crates.mapToSet { it.contentRootUrl }
             val (stdPackagesData, otherPackagesData) = oldPackagesData.partition { it.contentRootUrl in stdCratePackageRoots }
             val stdPackagesByPackageRoot = stdPackagesData.associateBy { it.contentRootUrl }
-            val pkgIdMapping = stdlib.crates.associate { it.id to (stdPackagesByPackageRoot[it.contentRootUrl]?.id ?: it.id) }
+            val pkgIdMapping = stdlib.crates.associate {
+                it.id to (stdPackagesByPackageRoot[it.contentRootUrl]?.id ?: it.id)
+            }
             val newStdlibCrates = stdlib.crates.map { it.copy(id = pkgIdMapping.getValue(it.id)) }
             val newStdlibDependencies = stdlib.dependencies.map { (oldId, dependency) ->
                 val newDependencies = dependency.mapToSet { it.copy(id = pkgIdMapping.getValue(it.id)) }
@@ -505,7 +512,7 @@ private class WorkspaceImpl(
 
 private class PackageImpl(
     override val workspace: WorkspaceImpl,
-    val id: PackageId,
+    override val id: PackageId,
     // Note: In tests, we use in-memory file system,
     // so we can't use `Path` here.
     val contentRootUrl: String,
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/RsExecutableRunner.kt b/src/main/kotlin/org/rust/cargo/runconfig/RsExecutableRunner.kt
index 1f55846fa..22cfd0d83 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/RsExecutableRunner.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/RsExecutableRunner.kt
@@ -16,15 +16,19 @@ import com.intellij.execution.ui.RunContentDescriptor
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.ui.Messages
 import com.intellij.openapi.util.Key
+import org.rust.cargo.project.model.cargoProjects
 import org.rust.cargo.runconfig.buildtool.CargoBuildManager.getBuildConfiguration
 import org.rust.cargo.runconfig.buildtool.CargoBuildManager.isBuildConfiguration
 import org.rust.cargo.runconfig.buildtool.CargoBuildManager.isBuildToolWindowEnabled
 import org.rust.cargo.runconfig.buildtool.cargoPatches
 import org.rust.cargo.runconfig.command.CargoCommandConfiguration
+import org.rust.cargo.toolchain.CargoCommandLine
+import org.rust.cargo.toolchain.impl.CargoMetadata
 import org.rust.cargo.toolchain.tools.Cargo.Companion.getCargoCommonPatch
 import org.rust.cargo.toolchain.tools.RsTool.Companion.createGeneralCommandLine
 import org.rust.cargo.util.CargoArgsParser.Companion.parseArgs
 import org.rust.openapiext.computeWithCancelableProgress
+import org.rust.stdext.toPath
 import java.nio.file.Path
 import java.util.concurrent.CompletableFuture
 
@@ -53,14 +57,15 @@ abstract class RsExecutableRunner(
             return
         }
         environment.cargoPatches += getCargoCommonPatch(project)
-        environment.putUserData(BINARIES, CompletableFuture())
+        environment.putUserData(ARTIFACT, CompletableFuture())
         super.execute(environment)
     }
 
     override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? {
         if (state !is CargoRunStateBase) return null
 
-        val binaries = environment.binaries.orEmpty()
+        val artifact = environment.artifact
+        val binaries = artifact?.executables.orEmpty()
         val errorMessage = when {
             binaries.isEmpty() -> "Can't find a binary."
             binaries.size > 1 -> "More than one binary was produced. " +
@@ -73,10 +78,11 @@ abstract class RsExecutableRunner(
         }
 
         val runCargoCommand = state.prepareCommandLine()
+        val workingDirectory = getWorkingDirectory(environment.project, runCargoCommand, artifact)
         val (_, executableArguments) = parseArgs(runCargoCommand.command, runCargoCommand.additionalArguments)
         val runExecutable = createGeneralCommandLine(
-            binaries.single(),
-            runCargoCommand.workingDirectory,
+            binaries.single().toPath(),
+            workingDirectory,
             runCargoCommand.redirectInputFrom,
             runCargoCommand.backtraceMode,
             runCargoCommand.environmentVariables,
@@ -86,6 +92,19 @@ abstract class RsExecutableRunner(
         return showRunContent(state, environment, runExecutable)
     }
 
+    private fun getWorkingDirectory(
+        project: Project,
+        runCargoCommand: CargoCommandLine,
+        artifact: CargoMetadata.Artifact?
+    ): Path {
+        if (runCargoCommand.command != "test") return runCargoCommand.workingDirectory
+        val packageId = artifact?.package_id ?: return runCargoCommand.workingDirectory
+        val pkg = project.cargoProjects.allProjects
+            .mapNotNull { it.workspace?.findPackageById(packageId) }
+            .firstOrNull()
+        return pkg?.rootDirectory ?: runCargoCommand.workingDirectory
+    }
+
     protected open fun showRunContent(
         state: CargoRunStateBase,
         environment: ExecutionEnvironment,
@@ -127,12 +146,13 @@ abstract class RsExecutableRunner(
     }
 
     companion object {
-        private val BINARIES: Key<CompletableFuture<List<Path>>> = Key.create("CARGO.CONFIGURATION.BINARIES")
+        private val ARTIFACT: Key<CompletableFuture<CargoMetadata.Artifact>> =
+            Key.create("CARGO.CONFIGURATION.ARTIFACT")
 
-        var ExecutionEnvironment.binaries: List<Path>?
-            get() = getUserData(this@Companion.BINARIES)?.get()
+        var ExecutionEnvironment.artifact: CargoMetadata.Artifact?
+            get() = getUserData(this@Companion.ARTIFACT)?.get()
             set(value) {
-                getUserData(this@Companion.BINARIES)?.complete(value)
+                getUserData(this@Companion.ARTIFACT)?.complete(value)
             }
     }
 }
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildContext.kt b/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildContext.kt
index 1ed400c30..2f2d2e4ec 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildContext.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/buildtool/CargoBuildContext.kt
@@ -16,9 +16,10 @@ import com.intellij.openapi.util.UserDataHolderEx
 import com.intellij.openapiext.isUnitTestMode
 import com.intellij.util.concurrency.FutureResult
 import org.rust.cargo.project.model.CargoProject
-import org.rust.cargo.runconfig.RsExecutableRunner.Companion.binaries
+import org.rust.cargo.runconfig.RsExecutableRunner.Companion.artifact
 import org.rust.cargo.runconfig.buildtool.CargoBuildManager.showBuildNotification
 import org.rust.cargo.runconfig.command.workingDirectory
+import org.rust.cargo.toolchain.impl.CargoMetadata
 import java.nio.file.Path
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
@@ -55,7 +56,7 @@ class CargoBuildContext(
     val warnings: AtomicInteger = AtomicInteger()
 
     @Volatile
-    var binaries: List<Path> = emptyList()
+    var artifact: CargoMetadata.Artifact? = null
 
     fun waitAndStart(): Boolean {
         indicator?.pushState()
@@ -82,7 +83,7 @@ class CargoBuildContext(
     fun finished(isSuccess: Boolean) {
         val isCanceled = indicator?.isCanceled ?: false
 
-        environment.binaries = binaries.takeIf { isSuccess && !isCanceled }
+        environment.artifact = artifact.takeIf { isSuccess && !isCanceled }
 
         finished = System.currentTimeMillis()
         buildSemaphore.release()
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/buildtool/RsBuildEventsConverter.kt b/src/main/kotlin/org/rust/cargo/runconfig/buildtool/RsBuildEventsConverter.kt
index 7e7cf0ac5..382f2aefd 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/buildtool/RsBuildEventsConverter.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/buildtool/RsBuildEventsConverter.kt
@@ -107,7 +107,7 @@ class RsBuildEventsConverter(private val context: CargoBuildContext) : BuildOutp
         }
         if (!isSuitableTarget || context.isTestBuild && !rustcArtifact.profile.test) return true
 
-        context.binaries = rustcArtifact.executables.map { Paths.get(it) }
+        context.artifact = rustcArtifact
 
         return true
     }
diff --git a/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt b/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt
index 8ae804e1e..88dd9c855 100644
--- a/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt
+++ b/src/main/kotlin/org/rust/cargo/runconfig/legacy/RsAsyncRunner.kt
@@ -185,7 +185,7 @@ abstract class RsAsyncRunner(
                             result = output.stdoutLines.asSequence()
                                 .mapNotNull { tryParseJsonObject(it) }
                                 .mapNotNull { CargoMetadata.Artifact.fromJson(it) }
-                                .filter { (target, profile) ->
+                                .filter { (_, target, profile) ->
                                     val isSuitableTarget = when (target.cleanKind) {
                                         CargoMetadata.TargetKind.BIN -> true
                                         CargoMetadata.TargetKind.EXAMPLE -> {
diff --git a/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt b/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt
index 840984e64..1d6320ff3 100644
--- a/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt
+++ b/src/main/kotlin/org/rust/cargo/toolchain/impl/CargoMetadata.kt
@@ -284,6 +284,7 @@ object CargoMetadata {
     // The next two things do not belong here,
     // see `machine_message` in Cargo.
     data class Artifact(
+        val package_id: PackageId?,
         val target: Target,
         val profile: Profile,
         val filenames: List<String>,
diff --git a/src/main/kotlin/org/rust/lang/core/resolve/NameResolution.kt b/src/main/kotlin/org/rust/lang/core/resolve/NameResolution.kt
index 7acce55f7..35d22c8f1 100644
--- a/src/main/kotlin/org/rust/lang/core/resolve/NameResolution.kt
+++ b/src/main/kotlin/org/rust/lang/core/resolve/NameResolution.kt
@@ -750,7 +750,7 @@ fun resolveStringPath(path: String, workspace: CargoWorkspace, project: Project)
     check(!path.startsWith("::"))
     val parts = path.split("::", limit = 2)
     if (parts.size != 2) return null
-    val pkg = workspace.findPackage(parts[0]) ?: run {
+    val pkg = workspace.findPackageByName(parts[0]) ?: run {
         return if (isUnitTestMode) {
             // Allows to set a fake path for some item in tests via
             // lang attribute, e.g. `#[lang = "std::iter::Iterator"]`
diff --git a/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt b/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt
index 7ce2d4fb3..8380d25cb 100644
--- a/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt
+++ b/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt
@@ -132,7 +132,7 @@ class RsExternalLinterPassTest : RsWithToolchainTestBase() {
         }.create()
 
         val path = project.cargoProjects.singleWorkspace()
-            .findPackage("rand")
+            .findPackageByName("rand")
             ?.contentRoot
             ?.findFileByRelativePath("src/lib.rs")
             ?: error("Can't find 'src/lib.rs' in 'rand' library")
diff --git a/toml/src/main/kotlin/org/rust/toml/resolve/CargoDependencyReferenceProvider.kt b/toml/src/main/kotlin/org/rust/toml/resolve/CargoDependencyReferenceProvider.kt
index 9ba88524f..da042ea8e 100644
--- a/toml/src/main/kotlin/org/rust/toml/resolve/CargoDependencyReferenceProvider.kt
+++ b/toml/src/main/kotlin/org/rust/toml/resolve/CargoDependencyReferenceProvider.kt
@@ -31,7 +31,7 @@ private class CargoDependencyReferenceImpl(key: TomlKeySegment) : PsiReferenceBa
         val project = element.project
         val file = element.containingFile?.virtualFile ?: return null
         val cargoProject = project.cargoProjects.findProjectForFile(file) ?: return null
-        val crateRoot = cargoProject.workspace?.findPackage(element.text)?.libTarget?.crateRoot ?: return null
+        val crateRoot = cargoProject.workspace?.findPackageByName(element.text)?.libTarget?.crateRoot ?: return null
         return crateRoot.toPsiFile(project) as? RsFile
     }