Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure TestModule, add RunModule #3064

Merged
merged 15 commits into from
Mar 20, 2024
Merged

Restructure TestModule, add RunModule #3064

merged 15 commits into from
Mar 20, 2024

Conversation

lefou
Copy link
Member

@lefou lefou commented Mar 5, 2024

This idea of this change is to restructure the TestModule to decouple it from the concept of compilation. After this change, only a runClasspath and a testClasspath is needed to run tests. This makes adding test modules for the sake of using a different test framework a breeze.

See the following example: the test2 module is an additional test framework on the same classes of test.

import mill._
import mill.scalalib._
import mill.scalalib.api.CompilationResult

object foo extends RootModule with ScalaModule {
  def scalaVersion = "2.13.11"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::scalatags:0.12.0",
    ivy"com.lihaoyi::mainargs:0.6.2"
  )

  object test extends ScalaTests {
    def ivyDeps = Agg(
      ivy"com.lihaoyi::utest:0.7.11",
      ivy"org.scalatest::scalatest-freespec:3.2.18"
    )
    def testFramework = "utest.runner.Framework"
  }
  object test2 extends TestModule with TestModule.ScalaTest {
    override def compile: T[CompilationResult] = ???
    override def runClasspath: T[Seq[PathRef]] = foo.test.runClasspath()
    override def testClasspath = foo.test.testClasspath()
  }
}

Please note the compile target is a legacy to our binary-compatibility promise. The target is not used directly in TestModule.

This pull request additionally contains the following changes:

  • Introduces a new RunModule and moved some run-related task previously in TestModule up.
  • Extend RunModule in JavaModule to share run-releated targets and resolve super-hierarchy
  • Introduces a WithZincWorker as a shared base trait to resolve super-hierarchies for using and overriding a common worker.

I plan to move more run-releated target from JavaModule to RunModule in a subsequent PR. (See #3090)

See also the following discussion:

@lefou lefou changed the title Introduce a TestModule.testClasspath to decouple from compilation Restructure TestModule to make it independent of compilation Mar 6, 2024
@lefou lefou changed the title Restructure TestModule to make it independent of compilation Restructure TestModule, add RunModule Mar 11, 2024
@lefou lefou marked this pull request as ready for review March 18, 2024 06:36
@lefou lefou requested review from lihaoyi and lolgab March 18, 2024 07:02
@lefou lefou mentioned this pull request Mar 18, 2024
1 task
*/
def testClasspath: T[Seq[PathRef]] = T {
// bin-compat-shim: keep the super.call in the classfile
super.testClasspath
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling super here but without calling apply, so even when not actual using the super-target here (we don't insert a real dependency to the super-target), we ensure the super-reference is generated in the byte-code. This allows us later to use the super-target without worrying about binary compatibility.

This is some new pattern I want to try out and establish. If it works out, we could automate it in the T-macros.

See for more details:

@lefou
Copy link
Member Author

lefou commented Mar 20, 2024

@lihaoyi , @lolgab , do you have any concerns about this pull request?

@lihaoyi
Copy link
Member

lihaoyi commented Mar 20, 2024

Looks good to me. I've needed this before: apart from the testFramework use case discussed earlier, I've needed to share the same compiled code but testing it with different flags or environment variables. It's possible to do today with a bunch of override hacks, but having TestModule be separated out is definitely a lot cleaner

@lefou
Copy link
Member Author

lefou commented Mar 20, 2024

I've needed this before: apart from the testFramework use case discussed earlier, I've needed to share the same compiled code but testing it with different flags or environment variables. It's possible to do today with a bunch of override hacks, but having TestModule be separated out is definitely a lot cleaner

That's probably even better covered by my other subsequent PR which is in the making:

It's exactly for the case, when you want run with different flags, envs, or main classes or classpaths.

@lefou lefou merged commit 6e0de1c into main Mar 20, 2024
38 checks passed
@lefou lefou deleted the test-classpath branch March 20, 2024 09:52
@lefou lefou added this to the 0.11.8 milestone Mar 20, 2024
lefou added a commit that referenced this pull request Mar 26, 2024
This PR moves the definition of the `run` targets from `JavaModule` into
`RunModule`. Also some other targets where moved, e.g. `mainClass`,
`finalMainClassOpt` and `finalMainClass`.

The advantage of having them in `RunModule` is, that one can easily
define and configure additional runners as sub-modules.

Example: Define a `daemon` sub-module which acts as runner for the
`cli.DaemonTool` class.

```scala
import mill._, scalalib._

object foo extends RootModule with JavaModule {
  def mainClass = "cli.Main"

  object daemon extends RunModule {
    def runClasspath = foo.runClasspath
    def localRunClasspath = foo.localRunClasspath
    def mainClass = "cli.DaemonTool"
  }
}
```

To preserve binary compatibility, all moved `def`s retain an override in
`JavaModule` and forward to their `super`-`def` in `RunModule`.
Therefore traits compiled against older versions of trait `JavaModule`
should still be runnable with newer versions.

Some `run`-targets (`run`, `runLocal` and `runBackground`) previously
required the `compile` target, since that also provided some zinc
analysis which included some main class discovery. Since the goal was to
decouple the `run` targets from the concept of compilation, I
implemented a different discovery logic for main classes which uses the
Scala API of `scalap`. This is completely newly written code but it's
only a couple of lines and all existing tests (which also include main
class discovery) succeeded. The advantage of the new logic is, that it
should work with any given classpath and also for non-Scala classes. The
new code is located in the zinc worker, since this is already shared
between all `RunModule`s, but there is no real need to have it there. To
avoid increased complexity, I resisted to introduce a new shared worker
just for the sake of technical independence, for now. The new
`allLocalMainClasses` target can be used to find all main classes in the
`localRunClasspath`. This is some useful information I needed now and
then in projects, so it makes sense to have it in a dedicated target for
better caching and easy `show`ing.

I also introduced a new `localRunClasspath` target which abstracts away
the classpath that is supposed to be produced by compilation. This is
somewhat analogue to the `testClassapth` introdcued in PR #3064. Since
both typically hold the same classpath, `testClasspath` by default uses
the result of `localRunClasspath`. We probably could remove
`testClasspath` altogether, but it's semantically bound to `TestModule`
and isn't necessarily the same as `localRunClasspath`, so I think it's
legit to keep it.

For consistency, I also added a `localCompileClasspath` which resembles
the part of the `localClasspath` which is feed into the compiler. All
added classpath targets also have a version for BSP (named with prefix
`bsp` and returning a `T[Agg[UnresolvedPath]]`) when appropriate.

Pull request: #3090
@chikei
Copy link
Contributor

chikei commented Apr 3, 2024

in case someone else trying out similar setup, in IJ you also need override def bspBuildTarget = super.bspBuildTarget.copy(canCompile = false, canRun = false, languageIds = Seq.empty) in the additional test module to prevent BuildTarget{Scalac, Javac}Options bsp requests which mill will eventually calls the dummy compile method and cause unimplemented exception.

@lefou
Copy link
Member Author

lefou commented Apr 3, 2024

in case someone else trying out similar setup, in IJ you also need override def bspBuildTarget = super.bspBuildTarget.copy(canCompile = false, canRun = false, languageIds = Seq.empty) in the additional test module to prevent BuildTarget{Scalac, Javac}Options bsp requests which mill will eventually calls the dummy compile method and cause unimplemented exception.

Nice catch, @chikei ! That was probably an oversight. Can you show your complete example? I wonder why the canCompile is true to start with.

@chikei
Copy link
Contributor

chikei commented Apr 4, 2024

Honestly I am not really sure if tweaking canCompile and canRun matters, IJ's code seems only care language id, but IJ's bsp import UI flow is so annoy I just don't bother to reduce.

As for canCompile = true, that's due to I used some other common trait which extends SbtModuleTests instead of the minimal TestModule with TestModule.ScalaTest showed here.

@lefou
Copy link
Member Author

lefou commented Apr 4, 2024

Honestly I am not really sure if tweaking canCompile and canRun matters, IJ's code seems only care language id, but IJ's bsp import UI flow is so annoy I just don't bother to reduce.

We currently don't check the canCompile before running the compilation in each module, we just communicate it to the BSP client. I think the client is supposed to not request compilation for that module at all.

As for canCompile = true, that's due to I used some other common trait which extends SbtModuleTests instead of the minimal TestModule with TestModule.ScalaTest showed here.

Ok. So I think if you stay with the simple example from this PR, canCompile should already be set to false. If that fails in IJ too, it's an issue worth to be reported there.

If you extends from an real language module like SbtModuleTests, which is set up to compile files, you either need to tweak the BSP settings when you customize the compile target or you need to implement the compile in a more defensive way. The refactoring of this PR is exactly the solution to this issue.

@chikei
Copy link
Contributor

chikei commented Apr 8, 2024

So I tried defining additional test without an real language module, this line in mill will throw a MatchError as the additional test module does not inherent JavaModule.

@lefou
Copy link
Member Author

lefou commented Apr 8, 2024

So I tried defining additional test without an real language module, this line in mill will throw a MatchError as the additional test module does not inherent JavaModule.

This is in fact a bug. I opened #3110 to track it.

@chikei
Copy link
Contributor

chikei commented Apr 10, 2024

So with PR #3111, I can make IJ import without error, but build project will fail as mill return statusCode = ERROR.

Skimming through IJ's BSP code, my understanding is that IJ ignores BuildTargetCapabilities properties returned from WorkspaceBuildTargets call, therefore build project in IJ results a BuildTargetCompile call with all BuildTarget fromWorkspaceBuildTargets, and mill reports failure for non JavaModule.

@lefou
Copy link
Member Author

lefou commented Apr 10, 2024

@chikei Thanks for the feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants