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

CommunityBuild: test community-code formatting #4256

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ jobs:
shell: bash
- run: TEST="2.13" sbt ci-test
shell: bash
community-test:
strategy:
fail-fast: false
matrix:
java: [ '11' ]
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JVM
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: 'sbt'
- run: sbt communityTests/test
formatting:
runs-on: ubuntu-latest
steps:
Expand Down
13 changes: 13 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,19 @@ lazy val tests = project.in(file("scalafmt-tests")).settings(
Seq[BuildInfoKey]("resourceDirectory" -> (Test / resourceDirectory).value),
).enablePlugins(BuildInfoPlugin).dependsOn(coreJVM, dynamic, cli)

lazy val communityTests = project.in(file("scalafmt-tests-community")).settings(
publish / skip := true,
libraryDependencies ++= Seq(
// Test dependencies
"com.lihaoyi" %% "scalatags" % "0.13.1",
scalametaTestkit,
munit.value,
),
scalacOptions ++= scalacJvmOptions.value,
javaOptions += "-Dfile.encoding=UTF8",
buildInfoPackage := "org.scalafmt.tests",
).enablePlugins(BuildInfoPlugin).dependsOn(coreJVM)

lazy val benchmarks = project.in(file("scalafmt-benchmarks")).settings(
publish / skip := true,
moduleName := "scalafmt-benchmarks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ case class RedundantBracesSettings(
object RedundantBracesSettings {

val default = RedundantBracesSettings()
private[scalafmt] val all =
RedundantBracesSettings(stringInterpolation = true, ifElseExpressions = true)

implicit lazy val surface: generic.Surface[RedundantBracesSettings] =
generic.deriveSurface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ case class RedundantParensSettings(

object RedundantParensSettings {
val default = RedundantParensSettings()
private[scalafmt] val all =
RedundantParensSettings(infixSide = Some(InfixSide.all))

implicit lazy val surface: generic.Surface[RedundantParensSettings] =
generic.deriveSurface
implicit lazy val codec: ConfCodecEx[RedundantParensSettings] = generic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,34 @@ case class ScalafmtConfig(
lazy val configStyleDefnSite: Newlines.ConfigStyleElement = newlines
.configStyleDefnSite.getOrElse(getOptInConfigStyle)

def forScalaJs: ScalafmtConfig = copy(
binPack = BinPack.always,
danglingParentheses = DanglingParentheses(false, false),
indent = Indents(callSite = 4, defnSite = 4),
importSelectors = ImportSelectors.binPack,
newlines = newlines.copy(
avoidInResultType = true,
neverBeforeJsNative = true,
sometimesBeforeColonInMethodReturnType = false,
),
// For some reason, the bin packing does not play nicely with forced
// config style. It's fixable, but I don't want to spend time on it
// right now.
runner = runner.conservative,
docstrings = docstrings.copy(style = Docstrings.Asterisk),
align = align.copy(
arrowEnumeratorGenerator = false,
tokens = Seq(AlignToken.caseArrow),
openParenCtrlSite = false,
),
)

def withAlign(align: Align): ScalafmtConfig = copy(align = align)

def withAlign(tokens: AlignToken*): ScalafmtConfig = withAlign(
align.copy(tokens = if (tokens.isEmpty) AlignToken.default else tokens),
)

}

object ScalafmtConfig {
Expand All @@ -323,34 +351,12 @@ object ScalafmtConfig {
danglingParentheses = DanglingParentheses.shortcutTrue,
)

def addAlign(style: ScalafmtConfig): ScalafmtConfig = style
.copy(align = style.align.copy(tokens = AlignToken.default))
val defaultWithAlign: ScalafmtConfig = addAlign(default)
val defaultWithAlign: ScalafmtConfig = default.withAlign(Align.more)

/** Experimental implementation of:
* https://github.com/scala-js/scala-js/blob/master/CODINGSTYLE.md
*/
val scalaJs: ScalafmtConfig = default.copy(
binPack = BinPack.always,
danglingParentheses = DanglingParentheses(false, false),
indent = Indents(callSite = 4, defnSite = 4),
importSelectors = ImportSelectors.binPack,
newlines = default.newlines.copy(
avoidInResultType = true,
neverBeforeJsNative = true,
sometimesBeforeColonInMethodReturnType = false,
),
// For some reason, the bin packing does not play nicely with forced
// config style. It's fixable, but I don't want to spend time on it
// right now.
runner = default.runner.conservative,
docstrings = default.docstrings.copy(style = Docstrings.Asterisk),
align = default.align.copy(
arrowEnumeratorGenerator = false,
tokens = Seq(AlignToken.caseArrow),
openParenCtrlSite = false,
),
)
val scalaJs: ScalafmtConfig = default.forScalaJs

/** Ready styles provided by scalafmt.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.scalafmt.community

import scala.meta._

case class CommunityBuild(
giturl: String,
commit: String,
name: String,
excluded: List[String],
checkedFiles: Int,
dialect: sourcecode.Text[Dialect],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.scalafmt.community

import scala.meta._

class CommunityMunitSuite extends CommunitySuite {

override protected def builds = Seq(
getBuild("v1.0.1", dialects.Scala213, 109),
// latest commit from 30.03.2021
getBuild("06346adfe3519c384201eec531762dad2f4843dc", dialects.Scala213, 102),
)

private def getBuild(
ref: String,
dialect: sourcecode.Text[Dialect],
files: Int,
) = CommunityBuild(
"https://github.com/scalameta/munit.git",
ref,
"munit",
Nil,
files,
dialect,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.scalafmt.community

import org.scalafmt.config._

import java.nio.file._

import scala.concurrent.duration
import scala.sys.process._

import munit.FunSuite

abstract class CommunitySuite extends FunSuite {

import TestHelpers._

override val munitTimeout = new duration.FiniteDuration(5, duration.MINUTES)

protected def builds: Seq[CommunityBuild]

for {
build <- builds
(k, v) <- TestStyles.styles
} {
val prefix = s"[ref ${build.commit}, style $k]"
val style: ScalafmtConfig = v.withDialect(NamedDialect(build.dialect))
test(s"community-build: ${build.name} $prefix")(check(k)(build, style))
}

private def check(
styleName: String,
)(implicit build: CommunityBuild, style: ScalafmtConfig): Unit = {
val folder = fetchCommunityBuild

val stats = checkFilesRecursive(styleName, folder.toAbsolutePath)
.getOrElse(TestStats.init)
val timePer1KLines = Math
.round(stats.timeTaken / (stats.linesParsed / 1000.0))

println("--------------------------")
println(build.name)
println(s"Files parsed correctly ${stats.checkedFiles - stats.errors}")
println(s"Files errored: ${stats.errors}")
println(s"Time taken: ${stats.timeTaken}ms")
if (stats.linesParsed < 1000) println(s"Lines parsed: ${stats.linesParsed}")
else println(s"Lines parsed: ~${stats.linesParsed / 1000}k")
println(s"Parsing speed per 1k lines ===> $timePer1KLines ms/1klines")
println("--------------------------")
stats.lastError.foreach(e => throw e)

assertEquals(stats.errors, 0)
assertEquals(
stats.checkedFiles,
build.checkedFiles * 2,
s"expected ${stats.checkedFiles / 2} per run",
)
}

private def fetchCommunityBuild(implicit build: CommunityBuild): Path = {
if (!Files.exists(communityDirectory)) Files
.createDirectory(communityDirectory)

val log = new StringBuilder
val logger = ProcessLogger(s => log.append(s).append('\n'))

def runCmd(cmd: String, what: => String): Unit = {
val result: Int = cmd.!(logger)
assertEquals(
clue(result),
0,
s"Community build ${build.name}: $what failed:\n$log",
)
log.clear()
}

val folderPath = communityDirectory.resolve(build.name)
val folder = folderPath.toString

if (!Files.exists(folderPath)) runCmd(
s"git clone --depth=1 --no-single-branch ${build.giturl} $folder",
"cloning",
)

val ref = build.commit

runCmd(s"git -C $folder fetch --depth=1 origin $ref", s"fetching [ref=$ref]")

runCmd(
s"git -C $folder checkout -f -B ref-$ref $ref",
s"checking out [ref=$ref]",
)

folderPath
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package org.scalafmt.community

import org.scalafmt.CompatCollections.JavaConverters._
import org.scalafmt.CompatCollections.ParConverters._
import org.scalafmt.Formatted
import org.scalafmt.Scalafmt
import org.scalafmt.config._

import scala.meta._

import java.io._
import java.nio.file._

import munit.ComparisonFailException
import munit.diff.Diff
import munit.diff.console.AnsiColors

object TestHelpers {

val communityDirectory = Paths
.get("scalafmt-tests-community/target/community-projects")

private val ignoreParts = List(
".git/",
"tests/",
"test/",
"test-resources/scripting/",
"test-resources/repl/",
"sbt-test/",
"out/",
).map(Paths.get(_))

private def timeIt[A](block: => A): (Long, A) = {
val t0 = System.currentTimeMillis()
val res = block
val t1 = System.currentTimeMillis()
(t1 - t0, res)
}

private def runFile(styleName: String, path: Path, absPathString: String)(
implicit style: ScalafmtConfig,
): TestStats = {
val input = Input.File(path).chars
val lines1 = input.count(_ == '\n')
val (duration1, result1) = timeIt(
Scalafmt.formatCode(new String(input), style, filename = absPathString),
)
result1.formatted match {
case x1: Formatted.Failure =>
println(s"Failed for original file $absPathString")
val trace = new StringWriter()
trace.append(x1.e.getMessage).append('\n')
x1.e.printStackTrace(new PrintWriter(trace))
println(s"Error: " + trace.toString)
TestStats(1, 1, Some(x1.e), duration1, lines1)
case x1: Formatted.Success =>
val stats1 = TestStats(1, 0, None, duration1, lines1)
val out1 = x1.formattedCode
val lines2 = x1.formattedCode.count(_ == '\n')
val (duration2, result2) =
timeIt(Scalafmt.formatCode(out1, style, filename = absPathString))
def saveFormatted(): Unit = Files.writeString(
Paths.get(absPathString + s".formatted.$styleName"),
out1,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
)
val stats2 = result2.formatted match {
case x2: Formatted.Failure =>
println(s"Failed for formatted file $absPathString")
println(s"Error: " + x2.e.getMessage)
saveFormatted()
TestStats(1, 1, Some(x2.e), duration2, lines2)
case x2: Formatted.Success =>
val out2 = x2.formattedCode
val diff = new Diff(out2, out1)
if (diff.isEmpty) TestStats(1, 0, None, duration2, lines2)
else {
val msg = AnsiColors.filterAnsi(diff.createDiffOnlyReport())
val loc = new munit.Location(absPathString, 0)
val exc = new ComparisonFailException(msg, out2, out1, loc, false)
println(s"Failed idempotency for file $absPathString")
println(msg)
saveFormatted()
TestStats(1, 1, Some(exc), duration2, lines2)
}
}
TestStats.merge(stats1, stats2)
}
}

def checkFilesRecursive(styleName: String, path: Path)(implicit
build: CommunityBuild,
style: ScalafmtConfig,
): Option[TestStats] =
if (ignoreParts.exists(path.endsWith)) None
else {
val ds = Files.newDirectoryStream(path)
val (dirs, files) =
try ds.iterator().asScala.toList.partition(Files.isDirectory(_))
finally ds.close()
val fileStats = files.par.flatMap { x =>
val fileStr = x.toString
if (fileStr.endsWith(".scala") && !excluded(fileStr))
Some(runFile(styleName, x, fileStr))
else None
}.reduceLeftOption(TestStats.merge)
val dirStats = dirs.par.flatMap(checkFilesRecursive(styleName, _))
.reduceLeftOption(TestStats.merge)
fileStats.fold(dirStats)(x =>
dirStats.map(TestStats.merge(_, x)).orElse(fileStats),
)
}

def excluded(path: String)(implicit build: CommunityBuild): Boolean = build
.excluded.exists(el => path.endsWith(el))

}
Loading
Loading