<commit_before>package dotty.communitybuild

import java.nio.file._
import java.io.{PrintWriter, File}
import java.nio.charset.StandardCharsets.UTF_8
import org.junit.{Ignore, Test}
import org.junit.Assert.{assertEquals, fail}
import org.junit.experimental.categories.Category

@Category(Array(classOf[TestCategory]))
class CommunityBuildTest { suite =>
  lazy val communitybuildDir: Path = Paths.get(sys.props("user.dir"))

  lazy val compilerVersion: String = {
    val file = communitybuildDir.resolve("dotty-bootstrapped.version")
    new String(Files.readAllBytes(file), UTF_8)
  }

  def log(msg: String) = println(Console.GREEN + msg + Console.RESET)

  /** Executes shell command, returns false in case of error. */
  def exec(projectDir: Path, binary: String, arguments: String*): Int = {
    val command = binary +: arguments
    log(command.mkString(" "))
    val builder = new ProcessBuilder(command: _*).directory(projectDir.toFile).inheritIO()
    val process = builder.start()
    val exitCode = process.waitFor()
    exitCode
  }

  def testSbt(project: String, testCommand: String, updateCommand: String, extraSbtArgs: Seq[String] = Nil) = {
    // Workaround for https://github.com/sbt/sbt/issues/4395
    new File(sys.props("user.home") + "/.sbt/1.0/plugins").mkdirs()
    val pluginFilePath = communitybuildDir.resolve("sbt-dotty-sbt").toAbsolutePath().toString()

    // Run the sbt command with the compiler version and sbt plugin set in the build
    val arguments = {
      val sbtProps = Option(System.getProperty("sbt.ivy.home")) match {
        case Some(ivyHome) =>
          Seq(s"-Dsbt.ivy.home=$ivyHome")
        case _ =>
          Seq()
      }
      extraSbtArgs ++ sbtProps ++ Seq(
        "-sbt-version", "1.2.7",
        s"--addPluginSbtFile=$pluginFilePath",
        s";clean ;set updateOptions in Global ~= (_.withLatestSnapshots(false)) ;++$compilerVersion! $testCommand"
      )
    }

    test(project, "sbt", arguments)
  }

  /** Build the given project with the published local compiler and sbt plugin.
   *
   *  This test reads the compiler version from community-build/dotty-bootstrapped.version
   *  and expects community-build/sbt-dotty-sbt to set the compiler plugin.
   *
   *  @param project        The project name, should be a git submodule in community-build/
   *  @param command        The sbt command used to test the project
   *  @param updateCommand  The sbt command used to update the project
   *  @param extraSbtArgs   Extra arguments to pass to sbt
   */
  def test(project: String, command: String, arguments: Seq[String]): Unit = {
    log(s"Building $project with dotty-bootstrapped $compilerVersion...")

    val projectDir = communitybuildDir.resolve("community-projects").resolve(project)

    if (!Files.exists(projectDir.resolve(".git"))) {
      fail(s"""
        |
        |Missing $project submodule. You can initialize this module using
        |
        |    git submodule update --init community-build/community-projects/$project
        |
        |""".stripMargin)
    }

    val exitCode = exec(projectDir, command, arguments: _*)

    if (exitCode != 0) {
      fail(s"""
          |
          |$command exited with an error code. To reproduce without JUnit, use:
          |
          |    sbt community-build/prepareCommunityBuild
          |    cd community-build/community-projects/$project
          |    $command ${arguments.init.mkString(" ")} "${arguments.last}"
          |
          |For a faster feedback loop on SBT projects, one can try to extract a direct call to dotc
          |using the sbt export command. For instance, for scalacheck, use
          |    sbt export jvm/test:compileIncremental
          |
          |""".stripMargin)
    }
  }

  case class MillCommunityProject(project: String, testCommand: String,
    publishCommand: String = "", extraArgs: List[String] = Nil,
    dependencies: List[MillCommunityProject] = Nil)
    final def test() =
      dependencies.foreach(_.publish())
      suite.test(project, "./mill", extraArgs :+ testCommand)

    final def publish() =
      log(s"Publishing $project")
      val projectDir = communitybuildDir.resolve("community-projects").resolve(project)
      if publishCommand.isEmpty
        throw RuntimeException(s"Missing publish command for project $this")
      exec(projectDir, "./mill", (extraArgs :+ publishCommand): _*)

  object projects {
    val utest = MillCommunityProject(
      project = "utest",
      testCommand = s"utest.jvm[$compilerVersion].test",
      publishCommand = s"utest.jvm[$compilerVersion].publishLocal",
      extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion")
    )

    val sourcecode = MillCommunityProject(
      project = "sourcecode",
      testCommand = s"sourcecode.jvm[$compilerVersion].test",
      publishCommand = s"sourcecode.jvm[$compilerVersion].publishLocal",
      extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion"),
    )

    val oslib = MillCommunityProject(
      project = "os-lib",
      testCommand = s"os[$compilerVersion].test",
      extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion"),
      dependencies = List(utest, sourcecode)
    )

    val oslibWatch = MillCommunityProject(
      project = "os-lib",
      testCommand = s"os.watch[$compilerVersion].test",
      extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion"),
      dependencies = List(utest, sourcecode)
    )
  }

  @Test def intent = testSbt(
    project       = "intent",
    testCommand   = "test",
    updateCommand = "update"
  )

  @Test def algebra = testSbt(<commit_msg>Avoid publishing the same project twice<commit_after>package dotty.communitybuild

import java.nio.file._
import java.io.{PrintWriter, File}
import java.nio.charset.StandardCharsets.UTF_8
import org.junit.{Ignore, Test}
import org.junit.Assert.{assertEquals, fail}
import org.junit.experimental.categories.Category

case class MillCommunityProject(project: String, testCommand: String,
  publishCommand: String = "", extraArgs: List[String] = Nil,
  dependencies: List[MillCommunityProject] = Nil)
  import communityBuildCommons.{ log, exec, communitybuildDir }

  private var published = false

  final def test()(given suite: CommunityBuildTest) =
    dependencies.foreach(_.publish())
    suite.test(project, "./mill", extraArgs :+ testCommand)

  final def publish() =
    if !published
      log(s"Publishing $project")
      val projectDir = communitybuildDir.resolve("community-projects").resolve(project)
      if publishCommand.isEmpty
        throw RuntimeException(s"Missing publish command for $project, project details:\n$this")
      val exitCode = exec(projectDir, "./mill", (extraArgs :+ publishCommand): _*)
      if exitCode != 0
        throw RuntimeException(s"Publish command exited with code $exitCode for project $project. Project details:\n$this")
      published = true
end MillCommunityProject

object communityBuildCommons
  lazy val communitybuildDir: Path = Paths.get(sys.props("user.dir"))

  lazy val compilerVersion: String = {
    val file = communitybuildDir.resolve("dotty-bootstrapped.version")
    new String(Files.readAllBytes(file), UTF_8)
  }

  def log(msg: String) = println(Console.GREEN + msg + Console.RESET)

  /** Executes shell command, returns false in case of error. */
  def exec(projectDir: Path, binary: String, arguments: String*): Int = {
    val command = binary +: arguments
    log(command.mkString(" "))
    val builder = new ProcessBuilder(command: _*).directory(projectDir.toFile).inheritIO()
    val process = builder.start()
    val exitCode = process.waitFor()
    exitCode
  }
end communityBuildCommons

object projects
  import communityBuildCommons._

  val utest = MillCommunityProject(
    project = "utest",
    testCommand = s"utest.jvm[$compilerVersion].test",
    publishCommand = s"utest.jvm[$compilerVersion].publishLocal",
    extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion")
  )

  val sourcecode = MillCommunityProject(
    project = "sourcecode",
    testCommand = s"sourcecode.jvm[$compilerVersion].test",
    publishCommand = s"sourcecode.jvm[$compilerVersion].publishLocal",
    extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion"),
  )

  val oslib = MillCommunityProject(
    project = "os-lib",
    testCommand = s"os[$compilerVersion].test",
    extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion"),
    dependencies = List(utest, sourcecode)
  )

  val oslibWatch = MillCommunityProject(
    project = "os-lib",
    testCommand = s"os.watch[$compilerVersion].test",
    extraArgs = List("-i", "-D", s"dottyVersion=$compilerVersion"),
    dependencies = List(utest, sourcecode)
  )
end projects

@Category(Array(classOf[TestCategory]))
class CommunityBuildTest {
  import communityBuildCommons._

  given CommunityBuildTest = this

  def testSbt(project: String, testCommand: String, updateCommand: String, extraSbtArgs: Seq[String] = Nil) = {
    // Workaround for https://github.com/sbt/sbt/issues/4395
    new File(sys.props("user.home") + "/.sbt/1.0/plugins").mkdirs()
    val pluginFilePath = communitybuildDir.resolve("sbt-dotty-sbt").toAbsolutePath().toString()

    // Run the sbt command with the compiler version and sbt plugin set in the build
    val arguments = {
      val sbtProps = Option(System.getProperty("sbt.ivy.home")) match {
        case Some(ivyHome) =>
          Seq(s"-Dsbt.ivy.home=$ivyHome")
        case _ =>
          Seq()
      }
      extraSbtArgs ++ sbtProps ++ Seq(
        "-sbt-version", "1.2.7",
        s"--addPluginSbtFile=$pluginFilePath",
        s";clean ;set updateOptions in Global ~= (_.withLatestSnapshots(false)) ;++$compilerVersion! $testCommand"
      )
    }

    test(project, "sbt", arguments)
  }

  /** Build the given project with the published local compiler and sbt plugin.
   *
   *  This test reads the compiler version from community-build/dotty-bootstrapped.version
   *  and expects community-build/sbt-dotty-sbt to set the compiler plugin.
   *
   *  @param project        The project name, should be a git submodule in community-build/
   *  @param command        The sbt command used to test the project
   *  @param updateCommand  The sbt command used to update the project
   *  @param extraSbtArgs   Extra arguments to pass to sbt
   */
  def test(project: String, command: String, arguments: Seq[String]): Unit = {
    log(s"Building $project with dotty-bootstrapped $compilerVersion...")

    val projectDir = communitybuildDir.resolve("community-projects").resolve(project)

    if (!Files.exists(projectDir.resolve(".git"))) {
      fail(s"""
        |
        |Missing $project submodule. You can initialize this module using
        |
        |    git submodule update --init community-build/community-projects/$project
        |
        |""".stripMargin)
    }

    val exitCode = exec(projectDir, command, arguments: _*)

    if (exitCode != 0) {
      fail(s"""
          |
          |$command exited with an error code. To reproduce without JUnit, use:
          |
          |    sbt community-build/prepareCommunityBuild
          |    cd community-build/community-projects/$project
          |    $command ${arguments.init.mkString(" ")} "${arguments.last}"
          |
          |For a faster feedback loop on SBT projects, one can try to extract a direct call to dotc
          |using the sbt export command. For instance, for scalacheck, use
          |    sbt export jvm/test:compileIncremental
          |
          |""".stripMargin)
    }
  }

  @Test def intent = testSbt(
    project       = "intent",
    testCommand   = "test",
    updateCommand = "update"
  )

  @Test def algebra = testSbt(
{'fraction_non_alphanumeric': 0.11963111604474426, 'fraction_numerical': 0.0021347451114336947, 'mean_word_length': 3.2450163102573395, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 8, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5937772', 'n_tokens_mistral': 3454, 'n_tokens_neox': 3362, 'n_words': 875}