Skip to content

Commit 5a5509b

Browse files
committed
Make Tests Parallel Again (MTPA)
1 parent a734489 commit 5a5509b

File tree

17 files changed

+182
-109
lines changed

17 files changed

+182
-109
lines changed

.github/workflows/nix.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ jobs:
1818
- name: Install TypeScript
1919
run: npm ci
2020
- name: Run test
21-
run: sbt -J-Xmx4096M -J-Xss8M test
21+
# Not running all tests because those outside of hkmc2 are obsolete (will be removed)
22+
run: sbt -J-Xmx4096M -J-Xss8M hkmc2AllTests/test
2223
- name: Check no changes
2324
run: |
2425
git update-index -q --refresh

README.md

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
11
# MLscript
22

3-
What would TypeScript look like if it had been designed with type inference and soundness in mind?
3+
This is the second iteration of the MLscript compiler,
4+
nicknamed _hkmc2_ (Hong Kong MLscript Compiler v2).
45

5-
We provide one possible answer in MLscript, an object-oriented and functional programming language with records, generic classes, mix-in traits, first-class unions and intersections, instance matching, and ML-style principal type inference.
6-
These features can be used to implement expressive class hierarchies as well as extensible sums and products.
7-
8-
MLscript supports union, intersection, and complement (or negation) connectives, making sure they form a Boolean algebra, and add enough structure to derive a sound and complete type inference algorithm.
96

107
## Getting Started
118

129
### Project Structure
1310

1411
#### Sub-Projects
1512

16-
- The ts2mls sub-project allows you to use TypeScript libraries in MLscript. It can generate libraries' declaration information in MLscript by parsing TypeScript AST, which can be used in MLscript type checking.
17-
18-
#### Directories
19-
20-
- The `shared/src/main/scala/mlscript` directory contains the sources of the MLscript compiler.
21-
22-
- The `shared/src/test/scala/mlscript` directory contains the sources of the testing infrastructure.
23-
24-
- The `shared/src/test/diff` directory contains the actual tests.
25-
26-
- The `ts2mls/js/src/main/scala/ts2mls` directory contains the sources of the ts2mls module.
27-
28-
- The `ts2mls/js/src/test/scala/ts2mls` directory contains the sources of the ts2mls declaration generation test code.
13+
Most SBT subprojects are obsolete and will be removed in the future.
2914

30-
- The `ts2mls/jvm/src/test/scala/ts2mls` directory contains the sources of the ts2mls diff test code.
31-
32-
- The `ts2mls/js/src/test/typescript` directory contains the TypeScript test code.
33-
34-
- The `ts2mls/js/src/test/diff` directory contains the declarations generated by ts2mls.
15+
Most of the important code of the new compiler is in the `hkmc2` folder.
3516

3617
### Prerequisites
3718

@@ -54,14 +35,30 @@ brew install mimalloc boost gmp
5435

5536
### Running the tests
5637

57-
Running the main MLscript tests only requires the Scala Build Tool installed.
58-
In the terminal, run `sbt mlscriptJVM/test`.
38+
Running the tests requires the Scala Build Tool (SBT) installed.
39+
40+
We recommend running all tests in the SBT shell,
41+
i.e., do not restart SBT every time,
42+
but launch it in shell mode (with command `sbt`)
43+
and then use one of the following commands.
44+
45+
- `hkmc2AllTests/test` for running all hkmc2 tests.
46+
- `hkmc2JVM/test` for running only the compilation tests, in `hkmc2/shared/src/test/mlscript-compile`.
47+
- `hkmc2DiffTests/test` for running only the diff-tests, in `hkmc2/shared/src/test/mlscript`.
48+
- `~hkmc2DiffTests/Test/run` for running the test watcher,
49+
which updates test files as you save them and recompiles the Scala sources automatically on change.
50+
- `test` for compiling all JVM and JS subprojects
51+
and running every single test in the repository,
52+
including obsolete ones.
5953

60-
Running the ts2mls MLscript tests requires NodeJS, and TypeScript in addition.
61-
In the terminal, run `sbt ts2mlsTest/test`.
54+
Another useful SBT incantation is `; hkmc2AllTests/test; ~hkmc2DiffTests/Test/run`.
55+
This command runs all hkmc2 tests once and then starts the test watcher.
56+
This is a useful command to use periodically while making changes to the compiler,
57+
to check that you haven't broken anything.
6258

63-
You can also run all tests simultaneously.
64-
In the terminal, run `sbt test`.
59+
Note that when saved, the special file `ChangedTests.cmd` will trigger the test watcher to run
60+
all tests that currently have unstaged changes in git.
61+
This is useful when you have a working subset of tests that you want to run periodically.
6562

6663
### Running tests individually
6764

@@ -89,6 +86,8 @@ private val testsData = List(
8986
)
9087
```
9188

89+
90+
<!--
9291
### Running the web demo locally
9392
9493
To run the demo on your computer, compile the project with `sbt fastOptJS`, then open the `local_testing.html` file in your browser.
@@ -98,3 +97,6 @@ in `shared/src/main/scala/mlscript`,
9897
have it compile to JavaScript on file change with command
9998
`sbt ~fastOptJS`,
10099
and immediately see the results in your browser by refreshing the page with `F5`.
100+
-->
101+
102+

build.sbt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ThisBuild / scalacOptions ++= Seq(
1919
)
2020

2121
lazy val root = project.in(file("."))
22-
.aggregate(mlscriptJS, mlscriptJVM, ts2mlsTest, compilerJVM, hkmc2JS, hkmc2JVM, coreJS, coreJVM)
22+
.aggregate(mlscriptJS, mlscriptJVM, ts2mlsTest, compilerJVM, hkmc2AllTests, coreJS, coreJVM)
2323
.settings(
2424
publish := {},
2525
publishLocal := {},
@@ -49,8 +49,6 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2"))
4949
baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript", "*.mls", NothingFilter),
5050
watchSources += WatchSource(
5151
baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript", "*.cmd", NothingFilter),
52-
53-
Test/run/fork := true, // so that CTRL+C actually terminates the watcher
5452
)
5553
.jvmSettings(
5654
)
@@ -59,6 +57,22 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2"))
5957
lazy val hkmc2JVM = hkmc2.jvm
6058
lazy val hkmc2JS = hkmc2.js
6159

60+
lazy val hkmc2DiffTests = project.in(file("hkmc2DiffTests"))
61+
.dependsOn(hkmc2JVM)
62+
.settings(
63+
scalaVersion := scala3Version,
64+
65+
libraryDependencies += "org.scalactic" %%% "scalactic" % "3.2.18",
66+
libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.18" % "test",
67+
68+
Test/run/fork := true, // so that CTRL+C actually terminates the watcher
69+
)
70+
71+
lazy val hkmc2AllTests = project.in(file("hkmc2AllTests"))
72+
.settings(
73+
Test / test := ((hkmc2DiffTests / Test / test) dependsOn (hkmc2JVM / Test / test)).value
74+
)
75+
6276
lazy val core = crossProject(JSPlatform, JVMPlatform).in(file("core"))
6377
.settings(
6478
sourceDirectory := baseDirectory.value.getParentFile()/"shared",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package hkmc2
2+
3+
import org.scalatest.{funsuite, ParallelTestExecution}
4+
import org.scalatest.time._
5+
import org.scalatest.concurrent.{TimeLimitedTests, Signaler}
6+
import os.up
7+
8+
import mlscript.utils._, shorthands._
9+
10+
11+
class CompileTestRunner
12+
extends funsuite.AnyFunSuite
13+
with ParallelTestExecution
14+
// with TimeLimitedTests // TODO
15+
:
16+
17+
private val inParallel = isInstanceOf[ParallelTestExecution]
18+
19+
// val timeLimit = TimeLimit
20+
21+
val pwd = os.pwd
22+
val workingDir = pwd
23+
24+
val dir = workingDir/"hkmc2"/"shared"/"src"/"test"
25+
26+
val validExt = Set("mls")
27+
28+
val allFiles = os.walk(dir)
29+
.filter(_.toIO.isFile)
30+
.filter(_.ext in validExt)
31+
32+
protected lazy val compileTestFiles = allFiles.filter: file =>
33+
file.segments.contains("mlscript-compile")
34+
35+
// TODO dedup path stuff with DiffTestRunner?
36+
compileTestFiles.foreach: file =>
37+
38+
val basePath = file.segments.drop(dir.segmentCount).toList.init
39+
val relativeName = basePath.map(_ + "/").mkString + file.baseName
40+
41+
test(relativeName):
42+
43+
println(s"Compiling: $relativeName")
44+
45+
val preludePath = dir/"mlscript"/"decls"/"Prelude.mls"
46+
47+
MLsCompiler(preludePath).compileModule(file)
48+
49+
end CompileTestRunner
50+
51+

hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ object Printer:
7878
val docBody = if publicFields.isEmpty && privateFields.isEmpty then doc"" else doc" { #{ ${docPrivFlds}${docPubFlds} #} # }"
7979
val docCtorParams = if clsParams.isEmpty then doc"" else doc"(${ctorParams.mkString(", ")})"
8080
doc"class ${sym.nme}${docCtorParams}${docBody}"
81-
81+
8282
def mkDocument(arg: Arg)(using Raise, Scope): Document =
8383
val doc = mkDocument(arg.value)
8484
if arg.spread

hkmc2/shared/src/test/mlscript/codegen/Pwd.mls

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
:sjs
66
let folderName1 = process.env.PWD.split("/").pop()
77
in let folderName2 = process.cwd().split("/").pop()
8-
in folderName2 === folderName1 || folderName2 === "jvm"
8+
in folderName2 === folderName1 || folderName2 === "shared"
99
//│ JS (unsanitized):
1010
//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6;
1111
//│ tmp = this.process.env.PWD.split("/") ?? null;
@@ -16,7 +16,7 @@ in folderName2 === folderName1 || folderName2 === "jvm"
1616
//│ tmp4 = tmp3.pop() ?? null;
1717
//│ this.folderName2 = tmp4;
1818
//│ tmp5 = this.folderName2 === this.folderName1;
19-
//│ tmp6 = this.folderName2 === "jvm";
19+
//│ tmp6 = this.folderName2 === "shared";
2020
//│ tmp5 || tmp6
2121
//│ = true
2222

hkmc2/jvm/src/test/scala/hkmc2/DiffMaker.scala renamed to hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,13 @@ abstract class DiffMaker:
120120

121121
val tests = Command("tests"):
122122
case "" =>
123-
new DiffTestRunner(
124-
// * I don't understand why when I use `new` here
125-
// * the test framework seems to reinstantiate `State` for every test
126-
// new DiffTestRunner.State
127-
DiffTestRunner.State
128-
){}.execute()
123+
// * Note that making `DiffTestRunnerBase` extend `ParallelTestExecution`,
124+
// * as we used to do, is quite dangerous, because of the way ScalaTest works (which is pretty dumb):
125+
// * it would try to re-instantiate the test classes haphazardly without passing it any arguments,
126+
// * which either crashes (as it would here) or recomputes the state every time
127+
// * (as would be the case if we created an anonymous subclass here),
128+
// * even when the tests, when run with `execute()`, are not run in parallel (also for dumb reasons).
129+
DiffTestRunnerBase(new DiffTestRunner.StateWithGit).execute()
129130

130131

131132
val fileName = file.last
@@ -198,7 +199,8 @@ abstract class DiffMaker:
198199
failures += globalStartLineNum
199200
unexpected("warning", blockLineNum, d.mkExtraInfo)
200201
case Diagnostic.Kind.Internal =>
201-
failures += globalStartLineNum
202+
if !tolerateErrors then
203+
failures += globalStartLineNum
202204
// unexpected("internal error", blockLineNum)
203205
throw d
204206
report(blockLineNum, d :: Nil, showRelativeLineNums.isSet)

0 commit comments

Comments
 (0)