├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── java │ └── com │ │ └── novocode │ │ └── junit │ │ ├── AbstractAnnotatedFingerprint.java │ │ ├── AbstractEvent.java │ │ ├── Ansi.java │ │ ├── EmptyRunner.java │ │ ├── EventDispatcher.java │ │ ├── GlobFilter.java │ │ ├── JUnit3Fingerprint.java │ │ ├── JUnitFingerprint.java │ │ ├── JUnitFramework.java │ │ ├── JUnitRunner.java │ │ ├── JUnitTask.java │ │ ├── OutputCapture.java │ │ ├── RichLogger.java │ │ ├── RunSettings.java │ │ ├── RunStatistics.java │ │ ├── RunWithFingerprint.java │ │ ├── SilentFilterRequest.java │ │ └── TestFilter.java └── ls │ └── 0.8.json └── sbt-test └── simple ├── can-run-single-test ├── build.sbt ├── src │ └── test │ │ └── scala │ │ └── test.scala └── test ├── categories ├── build.sbt ├── project │ └── Constants.scala ├── src │ └── test │ │ └── java │ │ └── test │ │ ├── Fast.java │ │ ├── Reporter.java │ │ ├── Slow.java │ │ ├── TestOne.java │ │ └── TestTwo.java └── test ├── check-test-selector ├── build.sbt ├── project │ └── build.properties ├── src │ └── test │ │ └── scala │ │ ├── CheckTestSelector.scala │ │ └── a │ │ └── b │ │ └── MyTestSuite.scala └── test ├── test-listener-multiple ├── build.sbt ├── src │ └── test │ │ ├── java │ │ └── test │ │ │ ├── JUnitListener1.java │ │ │ ├── JUnitListener2.java │ │ │ └── JUnitListenerBase.java │ │ └── scala │ │ └── test.scala └── test ├── test-listener ├── build.sbt ├── src │ └── test │ │ ├── java │ │ └── test │ │ │ └── JUnitListener.java │ │ └── scala │ │ └── test.scala └── test ├── test-report-ignored-tests ├── build.sbt ├── project │ └── build.properties ├── src │ └── test │ │ └── scala │ │ └── test.scala └── test ├── test-run-same-test-multiple-times ├── build.sbt ├── project │ └── build.properties ├── src │ └── test │ │ └── scala │ │ └── test.scala └── test └── tests-run-once ├── build.sbt ├── src └── test │ └── java │ ├── MyTest.java │ └── SuperclassTests.java └── test /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - os: ubuntu-latest 13 | java: 8 14 | jobtype: 1 15 | - os: ubuntu-latest 16 | java: 11 17 | jobtype: 1 18 | runs-on: ${{ matrix.os }} 19 | env: 20 | # define Java options for both official sbt and sbt-extras 21 | JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 22 | JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | - name: Setup 27 | uses: olafurpg/setup-scala@v10 28 | with: 29 | java-version: "adopt@1.${{ matrix.java }}" 30 | - name: Coursier cache 31 | uses: coursier/cache-action@v6 32 | - name: Build and test 33 | shell: bash 34 | run: | 35 | sbt -v test scripted 36 | rm -rf "$HOME/.ivy2/local" || true 37 | rm -r $(find $HOME/.sbt/boot -name "*-SNAPSHOT") || true 38 | find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true 39 | find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true 40 | find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true 41 | find $HOME/.sbt -name "*.lock" -delete || true 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | env: 10 | # define Java options for both official sbt and sbt-extras 11 | JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 12 | JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Setup 17 | uses: olafurpg/setup-scala@v10 18 | with: 19 | java-version: "adopt@1.8" 20 | - name: Coursier cache 21 | uses: coursier/cache-action@v5 22 | - name: Test 23 | run: | 24 | sbt test scripted packagedArtifacts 25 | - name: Release 26 | env: 27 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 28 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 29 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 30 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 31 | CI_CLEAN: clean 32 | CI_RELEASE: publishSigned 33 | run: | 34 | sbt ci-release 35 | rm -rf "$HOME/.ivy2/local" || true 36 | rm -r $(find $HOME/.sbt/boot -name "*-SNAPSHOT") || true 37 | find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true 38 | find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true 39 | find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true 40 | find $HOME/.sbt -name "*.lock" -delete || true 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | *.class 3 | /lib_managed 4 | project/boot 5 | target 6 | .scala_dependencies 7 | /\.target/ 8 | /\.cache 9 | /\.project 10 | /\.classpath 11 | /\.settings/ 12 | /\.manager/ 13 | /\.idea/ 14 | /\.idea_modules/ 15 | .metals/ 16 | .bloop/ 17 | .bsp/ 18 | .vscode/ 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2012, Stefan Zeiger 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JUnit Interface 2 | 3 | JUnit Interface is an implementation of [sbt's test interface](https://github.com/sbt/test-interface) for [JUnit 4](https://junit.org/junit4/). This allows you to run JUnit tests from [sbt](http://www.scala-sbt.org/). For JUnit 5, check out [maichler/sbt-jupiter-interface](https://github.com/maichler/sbt-jupiter-interface). 4 | 5 | Unlike Scala testing frameworks like ScalaTest (which can also run JUnit test cases), both JUnit and this adapter are pure Java, so you can run JUnit tests with any Scala version supported by sbt without having to build a binary-compatible test framework first. 6 | 7 | ### Setup 8 | 9 | Add the following dependency to your `build.sbt`: 10 | 11 | ```scala 12 | libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.2" % Test 13 | ``` 14 | 15 | **Note**: Organization name has changed to **`"com.github.sbt"`** in 0.12. 16 | 17 | JUnit itself is automatically pulled in as a transitive dependency. 18 | 19 | junit-interface version | JUnit version 20 | :-------------------------|:-------------- 21 | 0.13.2 | 4.13.2 22 | 0.13.1 | 4.13.1 23 | 0.13 | 4.13 24 | 0.12 | 4.12 25 | 26 | sbt already knows about junit-interface so the dependency alone is enough. You do not have to add it to the list of test frameworks. 27 | 28 | ### Usage 29 | 30 | To run the JUnit tests, type in `test` in the sbt shell: 31 | 32 | ``` 33 | > test 34 | ``` 35 | 36 | To run the tests incrementally, and run only the failed tests since the last run, type in `testQuick`: 37 | 38 | ``` 39 | > testQuick 40 | ``` 41 | 42 | To run only a specific test suite, use `testOnly `: 43 | 44 | ``` 45 | > testOnly example.HelloTest 46 | ``` 47 | 48 | You can use a glob pattern instead as: 49 | 50 | ``` 51 | > testOnly *.HelloTest 52 | ``` 53 | 54 | To run only a specific test example (a method) within a test suite, use `testOnly -- `: 55 | 56 | ``` 57 | > testOnly -- example.HelloTest.testSuccess1 58 | print from testSuccess1 59 | [info] Passed: Total 1, Failed 0, Errors 0, Passed 1 60 | ``` 61 | 62 | You can use the glob pattern here as well: 63 | 64 | ``` 65 | > testOnly -- *.HelloTest.testI* 66 | [info] Test example.HelloTest.testIgnored1 ignored 67 | [info] Test example.HelloTest.testIgnored2 ignored 68 | [info] Test example.HelloTest.testIgnored3 ignored 69 | ``` 70 | 71 | Note: Any arguments after `--` are treated as [test framework argument](https://www.scala-sbt.org/1.x/docs/Testing.html#Test+Framework+Arguments), and they are passed to junit-interface. Because `test` task cannot take any arguments, we can use `testOnly` task to pass ad-hoc options. 72 | 73 | To get run all tests with verbose output: 74 | 75 | ``` 76 | > testOnly -- -v 77 | print from testSuccess1 78 | [info] Test run example.HelloTest started 79 | [info] Test example.HelloTest.testSuccess1 started 80 | [info] Test example.HelloTest.testIgnored1 ignored 81 | [info] Test example.HelloTest.testIgnored2 ignored 82 | [info] Test example.HelloTest.testIgnored3 ignored 83 | [info] Test run example.HelloTest finished: 0 failed, 3 ignored, 1 total, 0.005s 84 | [info] Passed: Total 1, Failed 0, Errors 0, Passed 1, Ignored 3 85 | ``` 86 | 87 | The following options are supported for JUnit tests: 88 | 89 | Option | Description 90 | :---------------------------------------------|:---------------------- 91 | `-v` | Same as `--verbosity=2` 92 | `-q` | Suppress stdout for successful tests. Stderr is printed to the console normally. Stdout is written to a buffer and discarded when a test succeeds. If it fails, the buffer is dumped to the console. Since stdio redirection in Java is a bad kludge (`System.setOut()` changes the static final field System.out through native code) this may not work for all scenarios. Scala has its own console with a sane redirection feature. If Scala is detected on the class path, junit-interface tries to reroute scala.Console's stdout, too. 93 | `-n` | Do not use ANSI colors in the output even if sbt reports that they are supported. 94 | `-s` | Try to decode Scala names in stack traces and test names. Fall back silently to non-decoded names if no matching Scala library is on the class path. 95 | `-a` | Show stack traces and exception class name for AssertionErrors (thrown by all assert* methods in JUnit).` 96 | `-c` | Do not print the exception class name prefix for any messages. With this option, only the result of getMessage() plus a stack trace is shown. 97 | `+v` | Same as `--verbosity=0` 98 | `+q` | Turn off `-q`. Takes precedence over `-q`. 99 | `+n` | Turn off `-n`. Takes precedence over `-n`. 100 | `+s` | Turn off `-s`. Takes precedence over `-s`. 101 | `+a` | Turn off `-a`. Takes precedence over `-a`. 102 | `+c` | Turn off `-c`. Takes precedence over `-c`. 103 | `--ignore-runners=` | Ignore tests with a `@RunWith` annotation if the Runner class name is contained in this list. The default value is `org.junit.runners.Suite`. 104 | `--tests=` | Run only the tests whose names match one of the specified regular expressions (in a comma-separated list). Non-matched tests are ignored. Only individual test case names are matched, not test classes. Example: For test `MyClassTest.testBasic()` only "testBasic" is matched. Use sbt's `testOnly` task instead to match test classes. 105 | `-Dkey=value` | Temporarily set a system property for the duration of the test run. The property is restored to its previous value after the test has ended. Note that system properties are global to the entire JVM and they can be modified in a non-transactional way, so you should run tests serially and not perform any other tasks in parallel which depend on the modified property. 106 | `--run-listener=` | A (user defined) class which extends `org.junit.runner.notification.RunListener`. An instance of this class is created and added to the JUnit Runner, so that it will receive the run events. For more information, see [RunListener](http://junit.org/javadoc/latest/org/junit/runner/notification/RunListener.html). *Note: this uses the test-classloader, so the class needs to be defined in `src/test` or `src/main` or included as a test or compile dependency* 107 | `--include-categories=` | A comma separated list of category class names that should be included. Only tests with one or more of these categories will be run. 108 | `--exclude-categories=` | A comma separated list of category class names that should be excluded. No tests that match one or more of these categories will be run. 109 | `--verbosity=` | Higher verbosity logs more events at level "info" instead of "debug". 0: Default; 1: "Test run finished" at info; 2: Also "test run started" and "test started" at info; 3: Also "test finished" at info. 110 | `--summary=` | The type of summary to show for a test task execution. 0: Leave to sbt (default); 1: One line; 2: Include list of failed tests 111 | 112 | Any parameter not starting with `-` or `+` is treated as a glob pattern for matching tests. Unlike the patterns given directly to sbt's `testOnly` task, the patterns given to junit-interface will match against the full test names (as displayed by junit-interface) of all atomic test cases, so you can match on test methods and parts of suites with custom runners. 113 | 114 | You can set default options in your build.sbt file: 115 | 116 | ```scala 117 | testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v") 118 | ``` 119 | 120 | Or use them with the `testQuick` and `testOnly` commands: 121 | 122 | ``` 123 | > testOnly -- +q +v *Sequence*h2mem* 124 | ``` 125 | 126 | ### Credits 127 | 128 | - junit-interface was created by Stefan Zeiger @szeiger in 2009 129 | - See [contributors](https://github.com/sbt/junit-interface/graphs/contributors) for the list of other contributors 130 | 131 | ### License 132 | 133 | See LICENSE.txt for licensing conditions (BSD-style). 134 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := { 2 | val orig = (ThisBuild / version).value 3 | if (orig.endsWith("-SNAPSHOT")) "-SNAPSHOT" 4 | else orig 5 | } 6 | ThisBuild / organization := "com.github.sbt" 7 | ThisBuild / description := "An implementation of sbt's test interface for JUnit 4" 8 | ThisBuild / dynverSonatypeSnapshots := true 9 | 10 | lazy val `junit-interface` = (project in file(".")) 11 | .enablePlugins(SbtPlugin) 12 | .settings(nocomma { 13 | name := "junit-interface" 14 | 15 | autoScalaLibrary := false 16 | crossPaths := false 17 | sbtPlugin := false 18 | 19 | libraryDependencies ++= Seq( 20 | "junit" % "junit" % "4.13.2", 21 | "org.scala-sbt" % "test-interface" % "1.0", 22 | ) 23 | 24 | Compile / javacOptions ++= List("-target", "1.8", "-source", "1.8") 25 | 26 | // javadoc: error - invalid flag: -target. 27 | Compile / doc / javacOptions --= List("-target", "1.8") 28 | 29 | Test / publishArtifact := false 30 | 31 | scriptedBufferLog := false 32 | scriptedLaunchOpts ++= Seq( 33 | s"-Dplugin.version=${version.value}", 34 | "-Xmx256m" 35 | ) 36 | 37 | publishMavenStyle := true 38 | pomIncludeRepository := { _ => false } 39 | }) 40 | 41 | ThisBuild / publishTo := Some( 42 | if(version.value.trim.endsWith("SNAPSHOT")) "snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" 43 | else "releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2" 44 | ) 45 | ThisBuild / homepage := Some(url("http://github.com/sbt/junit-interface/")) 46 | ThisBuild / startYear := Some(2009) 47 | ThisBuild / licenses += ("Two-clause BSD-style license", url("http://github.com/sbt/junit-interface/blob/master/LICENSE.txt")) 48 | ThisBuild / developers := List( 49 | Developer( 50 | id = "szeiger", 51 | name = "Stefan Zeiger", 52 | email = "szeiger@novocode.com", 53 | url = url("http://szeiger.de") 54 | ), 55 | Developer( 56 | id = "eed3si9n", 57 | name = "Eugene Yokota", 58 | email = "@eed3si9n", 59 | url = url("https://eed3si9n.com/") 60 | ), 61 | ) 62 | ThisBuild / scmInfo := Some( 63 | ScmInfo( 64 | url("https://github.com/sbt/junit-interface"), 65 | "scm:git@github.com:sbt/junit-interface.git" 66 | ) 67 | ) 68 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.0") 2 | addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") 3 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/AbstractAnnotatedFingerprint.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import sbt.testing.AnnotatedFingerprint; 4 | 5 | public abstract class AbstractAnnotatedFingerprint implements AnnotatedFingerprint { 6 | @Override 7 | public boolean equals(Object obj) { 8 | if (!(obj instanceof AnnotatedFingerprint)) return false; 9 | AnnotatedFingerprint f = (AnnotatedFingerprint) obj; 10 | return annotationName().equals(f.annotationName()) && isModule() == f.isModule(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/AbstractEvent.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import sbt.testing.*; 4 | 5 | import static com.novocode.junit.Ansi.*; 6 | 7 | abstract class AbstractEvent implements Event 8 | { 9 | protected final String ansiName; 10 | protected final String ansiMsg; 11 | protected final Status status; 12 | protected final Throwable error; 13 | private final Fingerprint fingerprint; 14 | private final Long duration; 15 | 16 | AbstractEvent(String ansiName, String ansiMsg, Status status, Fingerprint fingerprint, Long duration, Throwable error) 17 | { 18 | this.fingerprint = fingerprint; 19 | this.ansiName = ansiName; 20 | this.ansiMsg = ansiMsg; 21 | this.status = status; 22 | this.duration = duration; 23 | this.error = error; 24 | } 25 | 26 | abstract void logTo(RichLogger logger); 27 | 28 | @Override 29 | public String fullyQualifiedName() { 30 | return filterAnsi(ansiName); 31 | } 32 | 33 | @Override 34 | public Fingerprint fingerprint() { 35 | return fingerprint; 36 | } 37 | 38 | @Override 39 | public Selector selector() { 40 | return new TestSelector(fullyQualifiedName()); 41 | } 42 | 43 | @Override 44 | public Status status() { 45 | return status; 46 | } 47 | 48 | @Override 49 | public OptionalThrowable throwable() { 50 | if( error == null ) { 51 | return new OptionalThrowable(); 52 | } else { 53 | return new OptionalThrowable(error); 54 | } 55 | } 56 | 57 | @Override 58 | public long duration() { 59 | return duration; 60 | } 61 | 62 | String durationToString() { 63 | return duration / 1000.0 + " sec"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/Ansi.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | public class Ansi { 4 | // Standard ANSI sequences 5 | private static final String NORMAL = "\u001B[0m"; 6 | private static final String HIGH_INTENSITY = "\u001B[1m"; 7 | private static final String LOW_INTESITY = "\u001B[2m"; 8 | private static final String BLACK = "\u001B[30m"; 9 | private static final String RED = "\u001B[31m"; 10 | private static final String GREEN = "\u001B[32m"; 11 | private static final String YELLOW = "\u001B[33m"; 12 | private static final String BLUE = "\u001B[34m"; 13 | private static final String MAGENTA = "\u001B[35m"; 14 | private static final String CYAN = "\u001B[36m"; 15 | private static final String WHITE = "\u001B[37m"; 16 | 17 | public static String c(String s, String colorSequence) 18 | { 19 | if(colorSequence == null) return s; 20 | else return colorSequence + s + NORMAL; 21 | } 22 | 23 | public static String filterAnsi(String s) 24 | { 25 | if(s == null) return null; 26 | StringBuilder b = new StringBuilder(s.length()); 27 | int len = s.length(); 28 | for(int i=0; i reported = Collections.newSetFromMap(new ConcurrentHashMap<>()); 23 | private final ConcurrentHashMap startTimes = new ConcurrentHashMap<>(); 24 | private final EventHandler handler; 25 | private final RunSettings settings; 26 | private final Fingerprint fingerprint; 27 | private final String taskInfo; 28 | private final RunStatistics runStatistics; 29 | private OutputCapture capture; 30 | 31 | EventDispatcher(RichLogger logger, EventHandler handler, RunSettings settings, Fingerprint fingerprint, 32 | Description taskDescription, RunStatistics runStatistics) 33 | { 34 | this.logger = logger; 35 | this.handler = handler; 36 | this.settings = settings; 37 | this.fingerprint = fingerprint; 38 | this.taskInfo = settings.buildInfoName(taskDescription); 39 | this.runStatistics = runStatistics; 40 | } 41 | 42 | private abstract class Event extends AbstractEvent { 43 | Event(String name, String message, Status status, Long duration, Throwable error) { 44 | super(name, message, status, fingerprint, duration, error); 45 | } 46 | String durationSuffix() { return ", took " + durationToString(); } 47 | } 48 | 49 | private abstract class ErrorEvent extends Event { 50 | ErrorEvent(Failure failure, Status status) { 51 | super(settings.buildErrorName(failure.getDescription()), 52 | settings.buildErrorMessage(failure.getException()), 53 | status, 54 | elapsedTime(failure.getDescription()), 55 | failure.getException()); 56 | } 57 | } 58 | 59 | private abstract class InfoEvent extends Event { 60 | InfoEvent(Description desc, Status status) { 61 | super(settings.buildInfoName(desc), null, status, elapsedTime(desc), null); 62 | } 63 | } 64 | 65 | @Override 66 | public void testAssumptionFailure(final Failure failure) 67 | { 68 | uncapture(true); 69 | postIfFirst(new ErrorEvent(failure, Status.Skipped) { 70 | void logTo(RichLogger logger) { 71 | logger.warn("Test assumption in test "+ansiName+" failed: "+ansiMsg + durationSuffix()); 72 | } 73 | }); 74 | } 75 | 76 | @Override 77 | public void testFailure(final Failure failure) 78 | { 79 | uncapture(true); 80 | postIfFirst(new ErrorEvent(failure, Status.Failure) { 81 | void logTo(RichLogger logger) { 82 | logger.error("Test "+ansiName+" failed: "+ansiMsg + durationSuffix(), error); 83 | } 84 | }); 85 | } 86 | 87 | @Override 88 | public void testFinished(Description desc) 89 | { 90 | uncapture(false); 91 | postIfFirst(new InfoEvent(desc, Status.Success) { 92 | void logTo(RichLogger logger) { 93 | debugOrInfo("Test "+ansiName+" finished" + durationSuffix(), RunSettings.Verbosity.TEST_FINISHED); 94 | } 95 | }); 96 | logger.popCurrentTestClassName(); 97 | } 98 | 99 | @Override 100 | public void testIgnored(Description desc) 101 | { 102 | postIfFirst(new InfoEvent(desc, Status.Ignored) { 103 | void logTo(RichLogger logger) { 104 | logger.info("Test "+ansiName+" ignored"); 105 | } 106 | }); 107 | } 108 | 109 | @Override 110 | public void testStarted(Description description) 111 | { 112 | recordStartTime(description); 113 | logger.pushCurrentTestClassName(description.getClassName()); 114 | debugOrInfo("Test " + settings.buildInfoName(description) + " started", RunSettings.Verbosity.STARTED); 115 | capture(); 116 | } 117 | 118 | private void recordStartTime(Description description) { 119 | startTimes.putIfAbsent(settings.buildPlainName(description), System.currentTimeMillis()); 120 | } 121 | 122 | private Long elapsedTime(Description description) { 123 | Long startTime = startTimes.get(settings.buildPlainName(description)); 124 | if( startTime == null ) { 125 | return 0L; 126 | } else { 127 | return System.currentTimeMillis() - startTime; 128 | } 129 | } 130 | 131 | @Override 132 | public void testRunFinished(Result result) 133 | { 134 | debugOrInfo(c("Test run ", INFO)+taskInfo+c(" finished: ", INFO)+ 135 | c(result.getFailureCount()+" failed", result.getFailureCount() > 0 ? ERRCOUNT : INFO)+ 136 | c(", ", INFO)+ 137 | c(result.getIgnoreCount()+" ignored", result.getIgnoreCount() > 0 ? IGNCOUNT : INFO)+ 138 | c(", "+result.getRunCount()+" total, "+ result.getRunTime() / 1000.0 +"s", INFO), RunSettings.Verbosity.RUN_FINISHED); 139 | runStatistics.addTime(result.getRunTime()); 140 | } 141 | 142 | @Override 143 | public void testRunStarted(Description description) 144 | { 145 | debugOrInfo(c("Test run ", INFO)+taskInfo+c(" started", INFO), RunSettings.Verbosity.STARTED); 146 | } 147 | 148 | void testExecutionFailed(String testName, Throwable err) 149 | { 150 | post(new Event(Ansi.c(testName, Ansi.ERRMSG), settings.buildErrorMessage(err), Status.Error, 0L, err) { 151 | void logTo(RichLogger logger) { 152 | logger.error("Execution of test "+ansiName+" failed: "+ansiMsg, error); 153 | } 154 | }); 155 | } 156 | 157 | private void postIfFirst(AbstractEvent e) 158 | { 159 | e.logTo(logger); 160 | 161 | String fqn = e.fullyQualifiedName(); 162 | if (reported.add(fqn)) { 163 | runStatistics.captureStats(e); 164 | handler.handle(e); 165 | } 166 | 167 | // NOTE: Status.Success is used to indicate that test is finished with any result (Success or Failure) 168 | // When test has failed, two events are actually generated: 169 | // 1) with Status.Failure 170 | // 2) with Status.Success (actually meaning that test has finished) 171 | // For non-failed tests, single event is emitted: Status.Success OR Status.Skipped OR Status.Ignored 172 | boolean testProcessed = e.status == Status.Success || e.status == Status.Skipped || e.status == Status.Ignored; 173 | if (testProcessed) { 174 | // JUnit can run tests with the same name multiple times: https://github.com/sbt/junit-interface/issues/96 175 | // Once test is finished, we mark it as unreported to allow running it again. 176 | // 177 | // There should be no issues with running tests in parallel (default behaviour of SBT) 178 | // For each JUnitTask a dedicated EventDispatcher will be created with it's own `reported` map 179 | reported.remove(fqn); 180 | } 181 | } 182 | 183 | private void post(AbstractEvent e) 184 | { 185 | e.logTo(logger); 186 | runStatistics.captureStats(e); 187 | handler.handle(e); 188 | } 189 | 190 | private void capture() 191 | { 192 | if(settings.quiet && capture == null) 193 | capture = OutputCapture.start(); 194 | } 195 | 196 | void uncapture(boolean replay) 197 | { 198 | if(settings.quiet && capture != null) 199 | { 200 | capture.stop(); 201 | if(replay) 202 | { 203 | try { capture.replay(); } 204 | catch(IOException ex) { logger.error("Error replaying captured stdio", ex); } 205 | } 206 | capture = null; 207 | } 208 | } 209 | 210 | private void debugOrInfo(String msg, RunSettings.Verbosity atVerbosity) 211 | { 212 | if(atVerbosity.ordinal() <= settings.verbosity.ordinal()) logger.info(msg); 213 | else logger.debug(msg); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/GlobFilter.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import java.util.ArrayList; 4 | import java.util.regex.Pattern; 5 | 6 | import org.junit.runner.Description; 7 | import org.junit.runner.manipulation.Filter; 8 | 9 | public final class GlobFilter extends Filter 10 | { 11 | private final ArrayList patterns = new ArrayList<>(); 12 | private final RunSettings settings; 13 | 14 | public GlobFilter(RunSettings settings, Iterable globPatterns) 15 | { 16 | this.settings = settings; 17 | for(String p : globPatterns) patterns.add(compileGlobPattern(p)); 18 | } 19 | 20 | @Override 21 | public String describe() { 22 | return "Filters out all tests not matched by the glob patterns"; 23 | } 24 | 25 | @Override 26 | public boolean shouldRun(Description d) 27 | { 28 | if(d.isSuite()) return true; 29 | String plainName = settings.buildPlainName(d); 30 | 31 | for(Pattern p : patterns) 32 | if(p.matcher(plainName).matches()) return true; 33 | 34 | return false; 35 | } 36 | 37 | private static Pattern compileGlobPattern(String expr) { 38 | String[] a = expr.split("\\*", -1); 39 | StringBuilder b = new StringBuilder(); 40 | for(int i=0; i runListeners; 22 | final RunStatistics runStatistics; 23 | 24 | JUnitRunner(String[] args, String[] remoteArgs, ClassLoader testClassLoader) { 25 | this.args = args; 26 | this.remoteArgs = remoteArgs; 27 | this.testClassLoader = testClassLoader; 28 | 29 | boolean quiet = false, nocolor = false, decodeScalaNames = false, 30 | logAssert = true, logExceptionClass = true; 31 | RunSettings.Verbosity verbosity = RunSettings.Verbosity.TERSE; 32 | RunSettings.Summary summary = RunSettings.Summary.SBT; 33 | HashMap sysprops = new HashMap<>(); 34 | ArrayList globPatterns = new ArrayList<>(); 35 | Set includeCategories = new HashSet<>(); 36 | Set excludeCategories = new HashSet<>(); 37 | 38 | String testFilter = ""; 39 | String ignoreRunners = "org.junit.runners.Suite"; 40 | final List runListeners = new ArrayList<>(); 41 | for(String s : args) { 42 | if("-q".equals(s)) quiet = true; 43 | else if("-v".equals(s)) verbosity = RunSettings.Verbosity.STARTED; 44 | else if("+v".equals(s)) verbosity = RunSettings.Verbosity.TERSE; 45 | else if(s.startsWith("--verbosity=")) verbosity = RunSettings.Verbosity.values()[Integer.parseInt(s.substring(12))]; 46 | else if(s.startsWith("--summary=")) summary = RunSettings.Summary.values()[Integer.parseInt(s.substring(10))]; 47 | else if("-n".equals(s)) nocolor = true; 48 | else if("-s".equals(s)) decodeScalaNames = true; 49 | else if("-a".equals(s)) logAssert = true; 50 | else if("-c".equals(s)) logExceptionClass = false; 51 | else if(s.startsWith("--tests=")) testFilter = s.substring(8); 52 | else if(s.startsWith("--ignore-runners=")) ignoreRunners = s.substring(17); 53 | else if(s.startsWith("--run-listener=")) runListeners.add(s.substring(15)); 54 | else if(s.startsWith("--include-categories=")) includeCategories.addAll(Arrays.asList(s.substring(21).split(","))); 55 | else if(s.startsWith("--exclude-categories=")) excludeCategories.addAll(Arrays.asList(s.substring(21).split(","))); 56 | else if(s.startsWith("-D") && s.contains("=")) { 57 | int sep = s.indexOf('='); 58 | sysprops.put(s.substring(2, sep), s.substring(sep+1)); 59 | } 60 | else if(!s.startsWith("-") && !s.startsWith("+")) globPatterns.add(s); 61 | } 62 | for(String s : args) { 63 | if("+q".equals(s)) quiet = false; 64 | else if("+n".equals(s)) nocolor = false; 65 | else if("+s".equals(s)) decodeScalaNames = false; 66 | else if("+a".equals(s)) logAssert = false; 67 | else if("+c".equals(s)) logExceptionClass = true; 68 | } 69 | this.settings = 70 | new RunSettings(!nocolor, decodeScalaNames, quiet, verbosity, summary, logAssert, ignoreRunners, logExceptionClass, 71 | sysprops, globPatterns, includeCategories, excludeCategories, 72 | testFilter); 73 | this.runListeners = runListeners.stream() 74 | .map(this::createRunListener) 75 | .collect(Collectors.toList()); 76 | this.runStatistics = new RunStatistics(settings); 77 | } 78 | 79 | @Override 80 | public Task[] tasks(TaskDef[] taskDefs) { 81 | used = true; 82 | Task[] tasks = Arrays 83 | .stream(taskDefs) 84 | .map(taskDef -> { 85 | RunSettings alteredSettings = alterRunSettings(this.settings, taskDef.selectors()); 86 | return new JUnitTask(this, alteredSettings, taskDef); 87 | }) 88 | .toArray(Task[]::new); 89 | return tasks; 90 | } 91 | 92 | /** 93 | * Alter default RunSettings depending on the passed selectors. 94 | * If selectors contains only elements of type TestSelector, then default settings are altered to include only test 95 | * names from these selectors. This allows to run particular test cases within given test class. 96 | * testFilter is treated as a regular expression, hence joining is done via '|'. 97 | */ 98 | private RunSettings alterRunSettings(RunSettings defaultSettings, Selector[] selectors) { 99 | boolean onlyTestSelectors = Arrays.stream(selectors).allMatch(selector -> selector instanceof TestSelector); 100 | if (onlyTestSelectors) { 101 | String testFilter = Arrays 102 | .stream(selectors) 103 | .map(selector -> ((TestSelector) selector).testName()) 104 | .collect(Collectors.joining("|")); 105 | // if already provided testFilter is not empty add to it | (regex or operator) 106 | String currentFilter = defaultSettings.testFilter.length() > 0 ? defaultSettings.testFilter + "|" : ""; 107 | String newFilter = currentFilter + testFilter; 108 | return defaultSettings.withTestFilter(newFilter); 109 | } 110 | 111 | return defaultSettings; 112 | } 113 | 114 | private RunListener createRunListener(String runListenerClassName) { 115 | if(runListenerClassName != null) { 116 | try { 117 | return (RunListener) testClassLoader.loadClass(runListenerClassName).newInstance(); 118 | } catch (Exception e) { 119 | throw new RuntimeException(e); 120 | } 121 | } else return null; 122 | } 123 | 124 | @Override 125 | public String done() { 126 | // Can't simply return the summary due to https://github.com/sbt/sbt/issues/3510 127 | if(!used) return ""; 128 | String stats = runStatistics.createSummary(); 129 | if(stats.isEmpty()) return stats; 130 | System.out.println(stats); 131 | return " "; 132 | } 133 | 134 | @Override 135 | public String[] remoteArgs() { 136 | return remoteArgs; 137 | } 138 | 139 | @Override 140 | public String[] args() { 141 | return args; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/JUnitTask.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import junit.framework.TestCase; 4 | import org.junit.experimental.categories.Categories; 5 | import org.junit.runner.Description; 6 | import org.junit.runner.JUnitCore; 7 | import org.junit.runner.Request; 8 | import org.junit.runner.RunWith; 9 | import sbt.testing.*; 10 | 11 | import java.lang.annotation.Annotation; 12 | import java.util.*; 13 | 14 | final class JUnitTask implements Task { 15 | private static final Fingerprint JUNIT_FP = new JUnitFingerprint(); 16 | 17 | private final JUnitRunner runner; 18 | private final RunSettings settings; 19 | private final TaskDef taskDef; 20 | 21 | public JUnitTask(JUnitRunner runner, RunSettings settings, TaskDef taskDef) { 22 | this.runner = runner; 23 | this.settings = settings; 24 | this.taskDef = taskDef; 25 | } 26 | 27 | @Override 28 | public String[] tags() { 29 | return new String[0]; // no tags yet 30 | } 31 | 32 | @Override 33 | public TaskDef taskDef() { return taskDef; } 34 | 35 | @Override 36 | public Task[] execute(EventHandler eventHandler, Logger[] loggers) { 37 | Fingerprint fingerprint = taskDef.fingerprint(); 38 | String testClassName = taskDef.fullyQualifiedName(); 39 | Description taskDescription = Description.createSuiteDescription(testClassName); 40 | RichLogger logger = new RichLogger(loggers, settings, testClassName); 41 | EventDispatcher ed = new EventDispatcher(logger, eventHandler, settings, fingerprint, taskDescription, runner.runStatistics); 42 | JUnitCore ju = new JUnitCore(); 43 | ju.addListener(ed); 44 | 45 | runner.runListeners.forEach(ju::addListener); 46 | 47 | Map oldprops = settings.overrideSystemProperties(); 48 | try { 49 | try { 50 | Class cl = runner.testClassLoader.loadClass(testClassName); 51 | if(shouldRun(fingerprint, cl, settings)) { 52 | Request request = Request.classes(cl); 53 | if(settings.globPatterns.size() > 0) { 54 | request = new SilentFilterRequest(request, new GlobFilter(settings, settings.globPatterns)); 55 | } 56 | if(settings.testFilter.length() > 0) { 57 | request = new SilentFilterRequest(request, new TestFilter(settings.testFilter, ed)); 58 | } 59 | if(!settings.includeCategories.isEmpty() || !settings.excludeCategories.isEmpty()) { 60 | request = new SilentFilterRequest(request, 61 | Categories.CategoryFilter.categoryFilter(true, loadClasses(runner.testClassLoader, settings.includeCategories), true, 62 | loadClasses(runner.testClassLoader, settings.excludeCategories))); 63 | } 64 | ju.run(request); 65 | } 66 | } catch(Exception ex) { 67 | ed.testExecutionFailed(testClassName, ex); 68 | } 69 | } finally { 70 | settings.restoreSystemProperties(oldprops); 71 | } 72 | return new Task[0]; // junit tests do not nest 73 | } 74 | 75 | private boolean shouldRun(Fingerprint fingerprint, Class clazz, RunSettings settings) { 76 | if(JUNIT_FP.equals(fingerprint)) { 77 | // Ignore classes which are matched by the other fingerprints 78 | if(TestCase.class.isAssignableFrom(clazz)) { 79 | return false; 80 | } 81 | for(Annotation a : clazz.getDeclaredAnnotations()) { 82 | if(a.annotationType().equals(RunWith.class)) return false; 83 | } 84 | return true; 85 | } else { 86 | RunWith rw = clazz.getAnnotation(RunWith.class); 87 | if(rw != null) { 88 | return !settings.ignoreRunner(rw.value().getName()); 89 | } 90 | else return true; 91 | } 92 | } 93 | 94 | private static Set> loadClasses(ClassLoader classLoader, Set classNames) throws ClassNotFoundException { 95 | Set> classes = new HashSet<>(); 96 | for(String className : classNames) { 97 | classes.add(classLoader.loadClass(className)); 98 | } 99 | return classes; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/OutputCapture.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.PrintStream; 6 | import java.lang.reflect.Method; 7 | 8 | final class OutputCapture 9 | { 10 | private final PrintStream originalOut = System.out; 11 | private final PrintStream originalScalaOut = getScalaOut(); 12 | private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 13 | private final PrintStream prBuffer = new PrintStream(buffer, true); 14 | private final boolean scalaOutSet; 15 | 16 | private OutputCapture() 17 | { 18 | //System.err.println("***** Replacing "+System.out+" with buffer "+prBuffer); 19 | System.out.flush(); 20 | System.setOut(prBuffer); 21 | scalaOutSet = setScalaOut(prBuffer); 22 | } 23 | 24 | static OutputCapture start() { return new OutputCapture(); } 25 | 26 | void stop() 27 | { 28 | //System.err.println("***** Restoring "+originalOut); 29 | System.out.flush(); 30 | System.setOut(originalOut); 31 | if(scalaOutSet) setScalaOut(originalScalaOut); 32 | } 33 | 34 | void replay() throws IOException 35 | { 36 | //System.err.println("***** Replaying to "+System.out); 37 | System.out.write(buffer.toByteArray()); 38 | System.out.flush(); 39 | } 40 | 41 | private static PrintStream getScalaOut() 42 | { 43 | try 44 | { 45 | Class cl = Class.forName("scala.Console"); 46 | Method m = cl.getMethod("out"); 47 | return (PrintStream)m.invoke(null); 48 | } 49 | catch(Throwable t) 50 | { 51 | //System.err.println("Error getting Scala console:"); 52 | //t.printStackTrace(System.err); 53 | return null; 54 | } 55 | } 56 | 57 | private static boolean setScalaOut(PrintStream p) 58 | { 59 | try 60 | { 61 | Class cl = Class.forName("scala.Console"); 62 | Method m = cl.getMethod("setOut", PrintStream.class); 63 | m.invoke(null, p); 64 | return true; 65 | } 66 | catch(Throwable t) 67 | { 68 | //System.err.println("Error setting Scala console:"); 69 | //t.printStackTrace(System.err); 70 | return false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/RichLogger.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import java.util.Stack; 4 | import sbt.testing.Logger; 5 | import static com.novocode.junit.Ansi.*; 6 | 7 | 8 | final class RichLogger 9 | { 10 | private final Logger[] loggers; 11 | private final RunSettings settings; 12 | /* The top element is the test class of the currently executing test */ 13 | private final Stack currentTestClassName = new Stack<>(); 14 | 15 | RichLogger(Logger[] loggers, RunSettings settings, String testClassName) 16 | { 17 | this.loggers = loggers; 18 | this.settings = settings; 19 | currentTestClassName.push(testClassName); 20 | } 21 | 22 | void pushCurrentTestClassName(String s) { currentTestClassName.push(s); } 23 | 24 | void popCurrentTestClassName() 25 | { 26 | if(currentTestClassName.size() > 1) currentTestClassName.pop(); 27 | } 28 | 29 | void debug(String s) 30 | { 31 | for(Logger l : loggers) 32 | if(settings.color && l.ansiCodesSupported()) l.debug(s); 33 | else l.debug(filterAnsi(s)); 34 | } 35 | 36 | void error(String s) 37 | { 38 | for(Logger l : loggers) 39 | if(settings.color && l.ansiCodesSupported()) l.error(s); 40 | else l.error(filterAnsi(s)); 41 | } 42 | 43 | void error(String s, Throwable t) 44 | { 45 | error(s); 46 | if(t != null && (settings.logAssert || !(t instanceof AssertionError))) logStackTrace(t); 47 | } 48 | 49 | void info(String s) 50 | { 51 | for(Logger l : loggers) 52 | if(settings.color && l.ansiCodesSupported()) l.info(s); 53 | else l.info(filterAnsi(s)); 54 | } 55 | 56 | void warn(String s) 57 | { 58 | for(Logger l : loggers) 59 | if(settings.color && l.ansiCodesSupported()) l.warn(s); 60 | else l.warn(filterAnsi(s)); 61 | } 62 | 63 | private void logStackTrace(Throwable t) 64 | { 65 | StackTraceElement[] trace = t.getStackTrace(); 66 | String testClassName = currentTestClassName.peek(); 67 | String testFileName = settings.color ? findTestFileName(trace, testClassName) : null; 68 | logStackTracePart(trace, trace.length-1, 0, t, testClassName, testFileName); 69 | } 70 | 71 | private void logStackTracePart(StackTraceElement[] trace, int m, int framesInCommon, Throwable t, String testClassName, String testFileName) 72 | { 73 | final int m0 = m; 74 | int top = 0; 75 | for(int i=top; i<=m; i++) 76 | { 77 | if(trace[i].toString().startsWith("org.junit.") || trace[i].toString().startsWith("org.hamcrest.")) 78 | { 79 | if(i == top) top++; 80 | else 81 | { 82 | m = i-1; 83 | while(m > top) 84 | { 85 | String s = trace[m].toString(); 86 | if(!s.startsWith("java.lang.reflect.") && !s.startsWith("sun.reflect.")) break; 87 | m--; 88 | } 89 | break; 90 | } 91 | } 92 | } 93 | for(int i=top; i<=m; i++) error(" at " + stackTraceElementToString(trace[i], testClassName, testFileName)); 94 | if(m0 != m) 95 | { 96 | // skip junit-related frames 97 | error(" ..."); 98 | } 99 | else if(framesInCommon != 0) 100 | { 101 | // skip frames that were in the previous trace too 102 | error(" ... " + framesInCommon + " more"); 103 | } 104 | logStackTraceAsCause(trace, t.getCause(), testClassName, testFileName); 105 | } 106 | 107 | private void logStackTraceAsCause(StackTraceElement[] causedTrace, Throwable t, String testClassName, String testFileName) 108 | { 109 | if(t == null) return; 110 | StackTraceElement[] trace = t.getStackTrace(); 111 | int m = trace.length - 1, n = causedTrace.length - 1; 112 | while(m >= 0 && n >= 0 && trace[m].equals(causedTrace[n])) 113 | { 114 | m--; 115 | n--; 116 | } 117 | error("Caused by: " + t); 118 | logStackTracePart(trace, m, trace.length-1-m, t, testClassName, testFileName); 119 | } 120 | 121 | private String findTestFileName(StackTraceElement[] trace, String testClassName) 122 | { 123 | for(StackTraceElement e : trace) 124 | { 125 | String cln = e.getClassName(); 126 | if(testClassName.equals(cln)) return e.getFileName(); 127 | } 128 | return null; 129 | } 130 | 131 | private String stackTraceElementToString(StackTraceElement e, String testClassName, String testFileName) 132 | { 133 | boolean highlight = settings.color && ( 134 | testClassName.equals(e.getClassName()) || 135 | testFileName != null && testFileName.equals(e.getFileName()) 136 | ); 137 | StringBuilder b = new StringBuilder(); 138 | b.append(settings.decodeName(e.getClassName() + '.' + e.getMethodName())); 139 | b.append('('); 140 | 141 | if(e.isNativeMethod()) b.append(c("Native Method", highlight ? TESTFILE2 : null)); 142 | else if(e.getFileName() == null) b.append(c("Unknown Source", highlight ? TESTFILE2 : null)); 143 | else 144 | { 145 | b.append(c(e.getFileName(), highlight ? TESTFILE1 : null)); 146 | if(e.getLineNumber() >= 0) 147 | b.append(':').append(c(String.valueOf(e.getLineNumber()), highlight ? TESTFILE2 : null)); 148 | } 149 | return b.append(')').toString(); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/RunSettings.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import static com.novocode.junit.Ansi.ENAME1; 4 | import static com.novocode.junit.Ansi.ENAME2; 5 | import static com.novocode.junit.Ansi.ENAME3; 6 | import static com.novocode.junit.Ansi.NNAME1; 7 | import static com.novocode.junit.Ansi.NNAME2; 8 | import static com.novocode.junit.Ansi.NNAME3; 9 | import static com.novocode.junit.Ansi.c; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.*; 13 | 14 | import org.junit.runner.Description; 15 | 16 | class RunSettings { 17 | private static final Object NULL = new Object(); 18 | 19 | final boolean color, quiet, logAssert, logExceptionClass; 20 | final Verbosity verbosity; 21 | final Summary summary; 22 | final ArrayList globPatterns; 23 | final Set includeCategories, excludeCategories; 24 | final String testFilter; 25 | 26 | private final boolean decodeScalaNames; 27 | private final HashMap sysprops; 28 | private final HashSet ignoreRunners = new HashSet<>(); 29 | 30 | RunSettings(boolean color, boolean decodeScalaNames, boolean quiet, 31 | Verbosity verbosity, Summary summary, boolean logAssert, String ignoreRunners, 32 | boolean logExceptionClass, 33 | HashMap sysprops, 34 | ArrayList globPatterns, 35 | Set includeCategories, Set excludeCategories, 36 | String testFilter) { 37 | this.color = color; 38 | this.decodeScalaNames = decodeScalaNames; 39 | this.quiet = quiet; 40 | this.verbosity = verbosity; 41 | this.summary = summary; 42 | this.logAssert = logAssert; 43 | this.logExceptionClass = logExceptionClass; 44 | for(String s : ignoreRunners.split(",")) 45 | this.ignoreRunners.add(s.trim()); 46 | this.sysprops = sysprops; 47 | this.globPatterns = globPatterns; 48 | this.includeCategories = includeCategories; 49 | this.excludeCategories = excludeCategories; 50 | this.testFilter = testFilter; 51 | } 52 | 53 | public RunSettings withTestFilter(String newTestFilter) { 54 | String ignoreRunners = String.join(",", this.ignoreRunners); 55 | return new RunSettings( 56 | this.color, this.decodeScalaNames, this.quiet, this.verbosity, this.summary, this.logAssert, 57 | ignoreRunners, this.logExceptionClass, this.sysprops, this.globPatterns, this.includeCategories, 58 | this.excludeCategories, newTestFilter 59 | ); 60 | } 61 | 62 | String decodeName(String name) { 63 | return decodeScalaNames ? decodeScalaName(name) : name; 64 | } 65 | 66 | private static String decodeScalaName(String name) { 67 | try { 68 | Class cl = Class.forName("scala.reflect.NameTransformer"); 69 | Method m = cl.getMethod("decode", String.class); 70 | String decoded = (String)m.invoke(null, name); 71 | return decoded == null ? name : decoded; 72 | } catch(Throwable t) { 73 | //System.err.println("Error decoding Scala name:"); 74 | //t.printStackTrace(System.err); 75 | return name; 76 | } 77 | } 78 | 79 | String buildInfoName(Description desc) { 80 | return buildColoredName(desc, NNAME1, NNAME2, NNAME3); 81 | } 82 | 83 | String buildErrorName(Description desc) { 84 | return buildColoredName(desc, ENAME1, ENAME2, ENAME3); 85 | } 86 | 87 | String buildPlainName(Description desc) { 88 | return buildColoredName(desc, null, null, null); 89 | } 90 | 91 | String buildColoredMessage(Throwable t, String c1) { 92 | if(t == null) return "null"; 93 | if(!logExceptionClass || !logAssert && t instanceof AssertionError) return t.getMessage(); 94 | StringBuilder b = new StringBuilder(); 95 | 96 | String cn = decodeName(t.getClass().getName()); 97 | int pos1 = cn.indexOf('$'); 98 | int pos2 = pos1 == -1 ? cn.lastIndexOf('.') : cn.lastIndexOf('.', pos1); 99 | if(pos2 == -1) b.append(c(cn, c1)); 100 | else { 101 | b.append(cn, 0, pos2); 102 | b.append('.'); 103 | b.append(c(cn.substring(pos2+1), c1)); 104 | } 105 | 106 | b.append(": ").append(t.getMessage()); 107 | return b.toString(); 108 | } 109 | 110 | String buildInfoMessage(Throwable t) { 111 | return buildColoredMessage(t, NNAME2); 112 | } 113 | 114 | String buildErrorMessage(Throwable t) { 115 | return buildColoredMessage(t, ENAME2); 116 | } 117 | 118 | private String buildColoredName(Description desc, String c1, String c2, String c3) { 119 | StringBuilder b = new StringBuilder(); 120 | 121 | String cn = decodeName(desc.getClassName()); 122 | int pos1 = cn.indexOf('$'); 123 | int pos2 = pos1 == -1 ? cn.lastIndexOf('.') : cn.lastIndexOf('.', pos1); 124 | if(pos2 == -1) b.append(c(cn, c1)); 125 | else { 126 | b.append(cn, 0, pos2); 127 | b.append('.'); 128 | b.append(c(cn.substring(pos2+1), c1)); 129 | } 130 | 131 | String m = desc.getMethodName(); 132 | if(m != null) { 133 | b.append('.'); 134 | int mpos1 = m.lastIndexOf('['); 135 | int mpos2 = m.lastIndexOf(']'); 136 | if(mpos1 == -1 || mpos2 < mpos1) b.append(c(decodeName(m), c2)); 137 | else { 138 | b.append(c(decodeName(m.substring(0, mpos1)), c2)); 139 | b.append('['); 140 | b.append(c(m.substring(mpos1+1, mpos2), c3)); 141 | b.append(']'); 142 | } 143 | } 144 | 145 | return b.toString(); 146 | } 147 | 148 | boolean ignoreRunner(String cln) { return ignoreRunners.contains(cln); } 149 | 150 | Map overrideSystemProperties() { 151 | HashMap oldprops = new HashMap<>(); 152 | synchronized(System.getProperties()) { 153 | for(Map.Entry me : sysprops.entrySet()) { 154 | String old = System.getProperty(me.getKey()); 155 | oldprops.put(me.getKey(), old == null ? NULL : old); 156 | } 157 | for(Map.Entry me : sysprops.entrySet()) { 158 | System.setProperty(me.getKey(), me.getValue()); 159 | } 160 | } 161 | return oldprops; 162 | } 163 | 164 | void restoreSystemProperties(Map oldprops) { 165 | synchronized(System.getProperties()) { 166 | for(Map.Entry me : oldprops.entrySet()) { 167 | if(me.getValue() == NULL) { 168 | System.clearProperty(me.getKey()); 169 | } else { 170 | System.setProperty(me.getKey(), (String)me.getValue()); 171 | } 172 | } 173 | } 174 | } 175 | 176 | enum Verbosity { 177 | TERSE, RUN_FINISHED, STARTED, TEST_FINISHED 178 | } 179 | 180 | enum Summary { 181 | SBT, ONE_LINE, LIST_FAILED 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/RunStatistics.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import sbt.testing.Status; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | class RunStatistics { 9 | private final RunSettings settings; 10 | 11 | private int failedCount, ignoredCount, otherCount; 12 | private final ArrayList failedNames = new ArrayList<>(); 13 | private volatile long accumulatedTime; 14 | 15 | RunStatistics(RunSettings settings) { 16 | this.settings = settings; 17 | } 18 | 19 | void addTime(long t) { accumulatedTime += t; } 20 | 21 | synchronized void captureStats(AbstractEvent e) { 22 | Status s = e.status(); 23 | if(s == Status.Error || s == Status.Failure) { 24 | failedCount++; 25 | failedNames.add(e.fullyQualifiedName()); 26 | } 27 | else { 28 | if(s == Status.Ignored) ignoredCount++; 29 | else otherCount++; 30 | } 31 | } 32 | 33 | private String summaryLine() { 34 | return (failedCount == 0 ? "All tests passed: " : "Some tests failed: ") + 35 | failedCount+" failed, "+ignoredCount+" ignored, "+(failedCount+ignoredCount+otherCount)+" total, "+ 36 | accumulatedTime / 1000.0 +"s"; 37 | } 38 | 39 | private static String mkString(List l) { 40 | StringBuilder b = new StringBuilder(); 41 | for(String s : l) { 42 | if(b.length() != 0) b.append(", "); 43 | b.append(s); 44 | } 45 | return b.toString(); 46 | } 47 | 48 | synchronized String createSummary() { 49 | switch(settings.summary) { 50 | case LIST_FAILED: 51 | return failedNames.isEmpty() ? 52 | summaryLine() : 53 | summaryLine() + "\n- Failed tests: " + mkString(failedNames); 54 | case ONE_LINE: 55 | return summaryLine(); 56 | default: 57 | return ""; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/RunWithFingerprint.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | public class RunWithFingerprint extends AbstractAnnotatedFingerprint { 4 | @Override 5 | public String annotationName() { return "org.junit.runner.RunWith"; } 6 | 7 | @Override 8 | public boolean isModule() { return false; } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/SilentFilterRequest.java: -------------------------------------------------------------------------------- 1 | package com.novocode.junit; 2 | 3 | import org.junit.runner.Request; 4 | import org.junit.runner.Runner; 5 | import org.junit.runner.manipulation.Filter; 6 | import org.junit.runner.manipulation.NoTestsRemainException; 7 | 8 | /** 9 | * A filtered request which ignores NoTestsRemainExceptions. 10 | */ 11 | final class SilentFilterRequest extends Request { 12 | private final Request request; 13 | private final Filter filter; 14 | 15 | public SilentFilterRequest(Request request, Filter filter) { 16 | this.request = request; 17 | this.filter = filter; 18 | } 19 | 20 | @Override 21 | public Runner getRunner() { 22 | Runner runner = request.getRunner(); 23 | try { 24 | filter.apply(runner); 25 | return runner; 26 | } catch (NoTestsRemainException e) { 27 | return new EmptyRunner(runner.getDescription()); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/com/novocode/junit/TestFilter.java: -------------------------------------------------------------------------------- 1 | // Mostly copied from http://stackoverflow.com/questions/1230706/running-a-subset-of-junit-test-methods/1236782#1236782 2 | package com.novocode.junit; 3 | 4 | import java.util.HashSet; 5 | import java.util.regex.Pattern; 6 | 7 | import org.junit.runner.Description; 8 | import org.junit.runner.manipulation.Filter; 9 | 10 | public final class TestFilter extends Filter 11 | { 12 | private static final String DELIMITER = ","; 13 | 14 | private final HashSet ignored = new HashSet<>(); 15 | private final String[] testPatterns; 16 | private final EventDispatcher ed; 17 | 18 | public TestFilter(String testFilter, EventDispatcher ed) 19 | { 20 | this.ed = ed; 21 | this.testPatterns = testFilter.split(DELIMITER); 22 | } 23 | 24 | @Override 25 | public String describe() 26 | { 27 | return "Filters out all tests not explicitly named in the '-tests=' option."; 28 | } 29 | 30 | @Override 31 | public boolean shouldRun(Description d) 32 | { 33 | String displayName = d.getDisplayName(); 34 | 35 | // We get asked both if we should run the class/suite, as well as the individual tests 36 | // So let the suite always run, so we can evaluate the individual test cases 37 | if(displayName.indexOf('(') == -1) return true; 38 | String testName = displayName.substring(0, displayName.indexOf('(')); 39 | 40 | // JUnit calls this multiple times per test and we don't want to print a new "test ignored" 41 | // message each time 42 | if(ignored.contains(testName)) return false; 43 | 44 | for(String p : testPatterns) 45 | if(Pattern.matches(p, testName)) return true; 46 | 47 | ignored.add(testName); 48 | ed.testIgnored(d); 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/ls/0.8.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization":"com.novocode", 3 | "name":"junit-interface", 4 | "version":"0.8", 5 | "description":"An implementation of sbt's test interface for JUnit 4. This allows you to run JUnit tests from sbt.", 6 | "site":"https://github.com/szeiger/junit-interface", 7 | "tags":["junit", "sbt"], 8 | "licenses": [{ 9 | "name": "BSD", 10 | "url": "https://github.com/szeiger/junit-interface/blob/635f723f1f6a8bc4f77ff95afa12ede2070e8e66/LICENSE.txt" 11 | }], 12 | "resolvers": ["http://scala-tools.org/repo-releases"], 13 | "dependencies": [{ 14 | "organization":"junit", 15 | "name": "junit", 16 | "version": "4.8.2" 17 | },{ 18 | "organization":"org.scala-tools.testing", 19 | "name": "test-interface", 20 | "version": "0.5" 21 | }], 22 | "sbt": true 23 | } 24 | -------------------------------------------------------------------------------- /src/sbt-test/simple/can-run-single-test/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-project" 2 | 3 | scalaVersion := "2.10.7" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | 7 | 8 | val checkTestDefinitions = taskKey[Unit]("Tests that the test is discovered properly") 9 | 10 | checkTestDefinitions := { 11 | val definitions = (definedTests in Test).value 12 | assert(definitions.length == 1, "Found more than the one test!") 13 | // TODO - Check fingerprint? 14 | streams.value.log.info("Test name = " + definitions.head.name) 15 | assert(definitions.head.name == "TestFoo", "Failed to discover/name the unit test!") 16 | } 17 | -------------------------------------------------------------------------------- /src/sbt-test/simple/can-run-single-test/src/test/scala/test.scala: -------------------------------------------------------------------------------- 1 | import org.junit._ 2 | 3 | class TestFoo { 4 | 5 | @Test 6 | def testFoo(): Unit = { 7 | import Assert._ 8 | assertEquals("Test should pass", true, true) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/sbt-test/simple/can-run-single-test/test: -------------------------------------------------------------------------------- 1 | # make sure we discover the unit tests appropriately 2 | > checkTestDefinitions 3 | # make sure the unit test passes 4 | > test 5 | # Debugging. 6 | > show definedTests 7 | -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-project" 2 | 3 | scalaVersion := "2.10.7" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | 7 | InputKey[Unit]("tests-executed") := { 8 | val expected = Def.spaceDelimited("").parsed 9 | val testsrun = IO.readLines(target.value / "testsrun").toSet 10 | expected.foreach { test => 11 | if (!testsrun(test)) { 12 | throw new RuntimeException("Expected test " + test + " to be run, but it wasn't. Tests that were run:\n" + testsrun.mkString("\n")) 13 | } 14 | } 15 | } 16 | 17 | InputKey[Unit]("tests-not-executed") := { 18 | val notExpected = Def.spaceDelimited("").parsed 19 | val testsrun = IO.readLines(target.value / "testsrun").toSet 20 | notExpected.foreach { test => 21 | if (testsrun(test)) { 22 | throw new RuntimeException("Expected test " + test + " not to be run, but it was. Tests that were run:\n" + testsrun.mkString("\n")) 23 | } 24 | } 25 | } 26 | 27 | TaskKey[Unit]("reset-tests") := { 28 | (target.value / "testsrun").delete() 29 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/project/Constants.scala: -------------------------------------------------------------------------------- 1 | // because it seems impossible in a set command to put a string literal in a scripted test 2 | object Constants { 3 | val IncludeFast = "--include-categories=test.Fast" 4 | val IncludeSlow = "--include-categories=test.Slow" 5 | val IncludeFastAndSlow = "--include-categories=test.Fast,test.Slow" 6 | val ExcludeFast = "--exclude-categories=test.Fast" 7 | val ExcludeSlow = "--exclude-categories=test.Slow" 8 | val ExcludeFastAndSlow = "--exclude-categories=test.Fast,test.Slow" 9 | } 10 | -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/src/test/java/test/Fast.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | public interface Fast {} -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/src/test/java/test/Reporter.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import java.io.*; 4 | 5 | public class Reporter { 6 | public static synchronized void report(String name) { 7 | try { 8 | Writer writer = new FileWriter("target/testsrun", true); 9 | writer.write(name + "\n"); 10 | writer.close(); 11 | } catch (Exception e) { 12 | throw new RuntimeException(e); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/src/test/java/test/Slow.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | public interface Slow {} -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/src/test/java/test/TestOne.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.*; 4 | import org.junit.Test; 5 | import org.junit.experimental.categories.*; 6 | 7 | public class TestOne { 8 | 9 | @Test 10 | @Category(Fast.class) 11 | public void fast() { 12 | Reporter.report("one-fast"); 13 | } 14 | 15 | @Test 16 | @Category(Slow.class) 17 | public void slow() { 18 | Reporter.report("one-slow"); 19 | } 20 | 21 | @Test 22 | public void none() { 23 | Reporter.report("one-none"); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/src/test/java/test/TestTwo.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.*; 4 | import org.junit.Test; 5 | import org.junit.experimental.categories.*; 6 | import org.junit.experimental.categories.Category; 7 | 8 | @Category(Fast.class) 9 | public class TestTwo { 10 | 11 | @Test 12 | public void a() { 13 | Reporter.report("two-a"); 14 | } 15 | 16 | @Test 17 | public void b() { 18 | Reporter.report("two-b"); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/categories/test: -------------------------------------------------------------------------------- 1 | # check everything runs 2 | > test 3 | > testsExecuted one-fast one-slow one-none two-a two-b 4 | 5 | > resetTests 6 | 7 | # Turn on fast category 8 | > set testOptions := Seq(Tests.Argument(TestFrameworks.JUnit, Constants.IncludeFast)) 9 | > test 10 | > testsExecuted one-fast two-a two-b 11 | > testsNotExecuted one-slow one-none 12 | 13 | > resetTests 14 | 15 | # Turn on slow category 16 | > set testOptions := Seq(Tests.Argument(TestFrameworks.JUnit, Constants.IncludeSlow)) 17 | > test 18 | > testsExecuted one-slow 19 | > testsNotExecuted one-fast one-none two-a two-b 20 | 21 | > resetTests 22 | 23 | # Turn on fast and slow category 24 | > set testOptions := Seq(Tests.Argument(TestFrameworks.JUnit, Constants.IncludeFastAndSlow)) 25 | > testOnly -- --summary=2 26 | > testsExecuted one-fast one-slow two-a two-b 27 | > testsNotExecuted one-none 28 | 29 | > resetTests 30 | 31 | # Exclude fast category 32 | > set testOptions := Seq(Tests.Argument(TestFrameworks.JUnit, Constants.ExcludeFast)) 33 | > test 34 | > testsExecuted one-slow one-none 35 | > testsNotExecuted one-fast two-a two-b 36 | 37 | > resetTests 38 | 39 | # Exclude slow category 40 | > set testOptions := Seq(Tests.Argument(TestFrameworks.JUnit, Constants.ExcludeSlow)) 41 | > test 42 | > testsExecuted one-fast one-none two-a two-b 43 | > testsNotExecuted one-slow 44 | 45 | > resetTests 46 | 47 | # Exclude slow and fast category 48 | > set testOptions := Seq(Tests.Argument(TestFrameworks.JUnit, Constants.ExcludeFastAndSlow)) 49 | > test 50 | > testsExecuted one-none 51 | > testsNotExecuted one-fast one-slow two-a two-b 52 | -------------------------------------------------------------------------------- /src/sbt-test/simple/check-test-selector/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-project" 2 | 3 | scalaVersion := "2.13.7" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | libraryDependencies += "org.scala-sbt" % "test-agent" % "1.5.5" % Test 7 | -------------------------------------------------------------------------------- /src/sbt-test/simple/check-test-selector/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.1 2 | -------------------------------------------------------------------------------- /src/sbt-test/simple/check-test-selector/src/test/scala/CheckTestSelector.scala: -------------------------------------------------------------------------------- 1 | import com.novocode.junit.JUnitFramework 2 | import com.novocode.junit.JUnitFingerprint 3 | 4 | import org.junit.Test 5 | import org.junit.Assert._ 6 | 7 | import sbt.testing._ 8 | import scala.collection.mutable.ArrayBuffer 9 | 10 | /** 11 | * Check if TestSelector's are correctly handled by JUnitRunner. 12 | * Execute prepared TaskDef's using manually created instances of sbt.testing.{Framework and Runner}. 13 | */ 14 | class CheckTestSelector { 15 | val framework = new JUnitFramework(); 16 | val runner = framework.runner( 17 | Array.empty[String], 18 | Array.empty[String], 19 | this.getClass().getClassLoader() 20 | ); 21 | 22 | private def getEventHandler(): (ArrayBuffer[String], EventHandler) = { 23 | val executedItems = new scala.collection.mutable.ArrayBuffer[String] 24 | val eventHandler = new EventHandler { 25 | override def handle(event: Event) = 26 | if (event.status() == Status.Success) { 27 | executedItems.addOne(event.fullyQualifiedName()) 28 | } 29 | } 30 | (executedItems, eventHandler) 31 | } 32 | 33 | private def getTaskDefs(selectors: Array[Selector]): Array[TaskDef] = { 34 | Array( 35 | new TaskDef("a.b.MyTestSuite", new JUnitFingerprint(), false, selectors) 36 | ) 37 | } 38 | 39 | @Test 40 | def runAllViaSuiteSelector() { 41 | val selectors = Array[Selector]( 42 | new SuiteSelector 43 | ) 44 | val taskDefs = Array( 45 | new TaskDef("a.b.MyTestSuite", new JUnitFingerprint(), false, selectors) 46 | ) 47 | 48 | val tasks = runner.tasks(taskDefs) 49 | assertEquals(tasks.size, 1) 50 | val task = tasks(0) 51 | 52 | val (executedItems, eventHandler) = getEventHandler() 53 | 54 | task.execute(eventHandler, Nil.toArray) 55 | assertArrayEquals( 56 | Array[Object]("a.b.MyTestSuite.testBar", "a.b.MyTestSuite.testFoo"), 57 | executedItems.toArray[Object] 58 | ) 59 | } 60 | 61 | @Test 62 | def runAllViaTestSelectors() { 63 | val selectors = Array[Selector]( 64 | new TestSelector("testFoo"), 65 | new TestSelector("testBar") 66 | ) 67 | val taskDefs = getTaskDefs(selectors) 68 | 69 | val tasks = runner.tasks(taskDefs) 70 | assertEquals(tasks.size, 1) 71 | val task = tasks(0) 72 | 73 | val (executedItems, eventHandler) = getEventHandler() 74 | 75 | task.execute(eventHandler, Nil.toArray) 76 | assertArrayEquals( 77 | Array[Object]("a.b.MyTestSuite.testBar", "a.b.MyTestSuite.testFoo"), 78 | executedItems.toArray[Object] 79 | ) 80 | } 81 | 82 | @Test 83 | def runOnlyOne() { 84 | val selectors = Array[Selector]( 85 | new TestSelector("testFoo") 86 | ) 87 | val taskDefs = getTaskDefs(selectors) 88 | 89 | val tasks = runner.tasks(taskDefs) 90 | assertEquals(tasks.size, 1) 91 | val task = tasks(0) 92 | 93 | val (executedItems, eventHandler) = getEventHandler() 94 | 95 | task.execute(eventHandler, Nil.toArray) 96 | assertArrayEquals( 97 | Array[Object]("a.b.MyTestSuite.testFoo"), 98 | executedItems.toArray[Object] 99 | ) 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/sbt-test/simple/check-test-selector/src/test/scala/a/b/MyTestSuite.scala: -------------------------------------------------------------------------------- 1 | package a.b 2 | 3 | import org.junit.Test 4 | import org.junit.Assert.assertEquals 5 | 6 | class MyTestSuite { 7 | 8 | @Test 9 | def testFoo(): Unit = { 10 | assertEquals("Test should pass", true, true) 11 | } 12 | 13 | @Test 14 | def testBar(): Unit = { 15 | assertEquals("Test should pass", true, true) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/sbt-test/simple/check-test-selector/test: -------------------------------------------------------------------------------- 1 | # make sure the unit test passes 2 | > test 3 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener-multiple/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-project" 2 | 3 | scalaVersion := "2.10.7" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % Test 6 | 7 | testOptions += Tests.Argument( 8 | TestFrameworks.JUnit, 9 | "-v", "-n", 10 | "--run-listener=test.JUnitListener1", 11 | "--run-listener=test.JUnitListener2" 12 | ) 13 | 14 | val listenerFile = settingKey[File]("location of the listener output") 15 | 16 | listenerFile := target.value / "listener.txt" 17 | 18 | javaOptions in Test += "-Djunit.output.file=" + listenerFile.value.getAbsolutePath 19 | 20 | fork in Test := true 21 | 22 | val checkRunListenerFile = taskKey[Unit]("Tests that the file is correct") 23 | 24 | checkRunListenerFile := { 25 | val expectedContent = 26 | """testStarted_1 testFail(TestFoo) 27 | |testStarted_2 testFail(TestFoo) 28 | |testFailure_1 testFail(TestFoo) 29 | |testFailure_2 testFail(TestFoo) 30 | |testFinished_1 testFail(TestFoo) 31 | |testFinished_2 testFail(TestFoo) 32 | |testStarted_1 testPass(TestFoo) 33 | |testStarted_2 testPass(TestFoo) 34 | |testFinished_1 testPass(TestFoo) 35 | |testFinished_2 testPass(TestFoo) 36 | |testRunFinished_1 37 | |testRunFinished_2""".stripMargin.replace("\r", "") 38 | 39 | val actualContent = sbt.IO.readLines(listenerFile.value).mkString("\n") 40 | assert( 41 | expectedContent == actualContent, 42 | s"""Expecting content: 43 | |$expectedContent 44 | |Actual content: 45 | |$actualContent""".stripMargin 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener-multiple/src/test/java/test/JUnitListener1.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | public class JUnitListener1 extends JUnitListenerBase { 4 | public JUnitListener1() { 5 | super("1"); 6 | } 7 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener-multiple/src/test/java/test/JUnitListener2.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | public class JUnitListener2 extends JUnitListenerBase { 4 | public JUnitListener2() { 5 | super("2"); 6 | } 7 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener-multiple/src/test/java/test/JUnitListenerBase.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.*; 4 | import java.io.*; 5 | import org.junit.runner.*; 6 | import org.junit.runner.notification.*; 7 | 8 | public abstract class JUnitListenerBase extends RunListener { 9 | private PrintWriter pw; 10 | private String outputFile = System.getProperty("junit.output.file"); 11 | 12 | private final String id; 13 | 14 | public JUnitListenerBase(String id) { 15 | this.id = id; 16 | } 17 | 18 | public void testRunStarted(Description description) throws Exception { 19 | File file = new File(outputFile); 20 | pw = new PrintWriter(new FileWriter(outputFile, /*append=*/true)); 21 | } 22 | public void testRunFinished(Result result) throws Exception { 23 | pw.println("testRunFinished_" + id); 24 | pw.close(); 25 | } 26 | public void testStarted(Description description) throws Exception { 27 | pw.println("testStarted_" + id + " " + description.getDisplayName()); 28 | pw.flush(); 29 | } 30 | public void testFinished(Description description) throws Exception { 31 | pw.println("testFinished_" + id + " " + description.getDisplayName()); 32 | pw.flush(); 33 | } 34 | public void testFailure(Failure failure) throws Exception { 35 | pw.println("testFailure_" + id + " " + failure.getDescription().getDisplayName()); 36 | pw.flush(); 37 | } 38 | public void testAssumptionFailure(Failure failure) { 39 | pw.print("ASSUMPTION FAILURE"); 40 | pw.flush(); 41 | } 42 | public void testIgnored(Description description) throws Exception { 43 | pw.println("testIgnored_" + id + " " + description.getDisplayName()); 44 | pw.flush(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener-multiple/src/test/scala/test.scala: -------------------------------------------------------------------------------- 1 | import org.junit._ 2 | 3 | class TestFoo { 4 | 5 | @Test 6 | def testPass(): Unit = { 7 | Thread.sleep(2000) 8 | import Assert._ 9 | assertEquals("Test should pass", true, true) 10 | } 11 | 12 | @Test 13 | def testFail(): Unit = { 14 | Thread.sleep(2000) 15 | import Assert._ 16 | assertEquals("Test should fail", fail, true) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener-multiple/test: -------------------------------------------------------------------------------- 1 | # test that the run listener option works. 2 | # one test passes, other fails 3 | -> test 4 | # Check that file has been created and is correct 5 | > checkRunListenerFile 6 | # Debugging. 7 | > show definedTests 8 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-project" 2 | 3 | scalaVersion := "2.10.7" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | 7 | testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-n", "--run-listener=test.JUnitListener") 8 | 9 | val listenerFile = settingKey[File]("location of the listener output") 10 | 11 | listenerFile := (target.value / "listener.txt") 12 | 13 | javaOptions in Test += "-Djunit.output.file=" + listenerFile.value.getAbsolutePath 14 | 15 | fork in Test := true 16 | 17 | val checkRunListenerFile = taskKey[Unit]("Tests that the file is correct") 18 | 19 | checkRunListenerFile := { 20 | val expectedContents = List("testRunStarted", 21 | "testStarted testFail(TestFoo)", 22 | "testFailure testFail(TestFoo)", 23 | "testFinished testFail(TestFoo)", 24 | "testStarted testPass(TestFoo)", 25 | "testFinished testPass(TestFoo)", 26 | "testRunFinished") 27 | val contents = sbt.IO.readLines(listenerFile.value) 28 | assert(expectedContents == contents, "Expecting contents=" + expectedContents + " actual=" + contents) 29 | } 30 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener/src/test/java/test/JUnitListener.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.*; 4 | import java.io.*; 5 | import org.junit.runner.*; 6 | import org.junit.runner.notification.*; 7 | 8 | public class JUnitListener extends RunListener { 9 | private PrintWriter pw; 10 | private String outputFile = System.getProperty("junit.output.file"); 11 | 12 | public void testRunStarted(Description description) throws Exception { 13 | pw = new PrintWriter(new FileWriter(outputFile)); 14 | pw.println("testRunStarted"); 15 | } 16 | public void testRunFinished(Result result) throws Exception { 17 | pw.println("testRunFinished"); 18 | pw.close(); 19 | } 20 | public void testStarted(Description description) throws Exception { 21 | pw.println("testStarted " + description.getDisplayName()); 22 | } 23 | public void testFinished(Description description) throws Exception { 24 | pw.println("testFinished " + description.getDisplayName()); 25 | } 26 | public void testFailure(Failure failure) throws Exception { 27 | pw.println("testFailure " + failure.getDescription().getDisplayName()); 28 | } 29 | public void testAssumptionFailure(Failure failure) { 30 | pw.print("ASSUMPTION FAILURE"); 31 | } 32 | public void testIgnored(Description description) throws Exception { 33 | pw.println("testIgnored " + description.getDisplayName()); 34 | } 35 | } -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener/src/test/scala/test.scala: -------------------------------------------------------------------------------- 1 | import org.junit._ 2 | 3 | class TestFoo { 4 | 5 | @Test 6 | def testPass(): Unit = { 7 | import Assert._ 8 | assertEquals("Test should pass", true, true) 9 | } 10 | 11 | @Test 12 | def testFail(): Unit = { 13 | import Assert._ 14 | assertEquals("Test should fail", fail, true) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-listener/test: -------------------------------------------------------------------------------- 1 | # test that the run listener option works. 2 | # one test passes, other fails 3 | -> test 4 | # Check that file has been created and is correct 5 | > checkRunListenerFile 6 | # Debugging. 7 | > show definedTests 8 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-report-ignored-tests/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-report-ignored-tests" 2 | 3 | scalaVersion := "2.13.6" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | 7 | val checkTestSummaryOutput = taskKey[Unit]("Check test summary output") 8 | 9 | checkTestSummaryOutput := { 10 | val commandOutput = testCommandOutput(target.value, "test") 11 | assertContainsLine(commandOutput, "[error] Failed: Total 5, Failed 3, Errors 0, Passed 2, Ignored 4") 12 | } 13 | 14 | def testCommandOutput(targetRoot: File, command: String): Seq[String] = { 15 | val raw = IO.readLines(targetRoot / "streams" / "test" / command / "_global" / "streams" / "out") 16 | raw.map(filterAnsi) 17 | } 18 | 19 | def assertContainsLine(output: Seq[String], expectedLine: String): Unit = { 20 | if (!output.contains(expectedLine)) { 21 | throw new AssertionError(s"!!!!! SBT output doesn't contain expected line: $expectedLine") 22 | } 23 | } 24 | 25 | def filterAnsi(text: String): String = 26 | text.replaceAll("\u001B\\[[\\d;]*[^\\d;]", "") 27 | 28 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-report-ignored-tests/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.5.2 -------------------------------------------------------------------------------- /src/sbt-test/simple/test-report-ignored-tests/src/test/scala/test.scala: -------------------------------------------------------------------------------- 1 | import org.junit._ 2 | import Assert._ 3 | 4 | class TestFoo { 5 | 6 | @Test def testSuccess1(): Unit = {} 7 | @Test def testSuccess2(): Unit = {} 8 | 9 | @Test def testFailure1(): Unit = { fail("fail reason") } 10 | @Test def testFailure2(): Unit = { fail("fail reason") } 11 | @Test def testFailure3(): Unit = { fail("fail reason") } 12 | 13 | @Test @Ignore def testIgnored1(): Unit = { fail("unreachable code") } 14 | @Test @Ignore def testIgnored2(): Unit = { fail("unreachable code") } 15 | @Test @Ignore def testIgnored3(): Unit = { fail("unreachable code") } 16 | @Test @Ignore def testIgnored4(): Unit = { fail("unreachable code") } 17 | } 18 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-report-ignored-tests/test: -------------------------------------------------------------------------------- 1 | # Test that `@Ignored` tests are reported as "ignored" and not "skipped" 2 | -> test 3 | > checkTestSummaryOutput -------------------------------------------------------------------------------- /src/sbt-test/simple/test-run-same-test-multiple-times/build.sbt: -------------------------------------------------------------------------------- 1 | name := "test-report-ignored-tests" 2 | 3 | scalaVersion := "2.13.6" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | 7 | // NOTE: by default junit-interface ignores `org.junit.runners.Suite`, 8 | // which is used in this integration test, 9 | // we need to disable this behaviour 10 | Test / testOptions += Tests.Argument("--ignore-runners="/*, "--verbosity=3"*/) 11 | 12 | val checkAfterTestOnly = taskKey[Unit]("checkAfterTestOnly") 13 | val checkAfterTest = taskKey[Unit]("checkAfterTest") 14 | 15 | checkAfterTestOnly := { 16 | val commandOutput = testCommandOutput(target.value, "testOnly") 17 | // contains 2x more tests that single `MyActualTest` 18 | assertContainsLine(commandOutput, "[error] Failed: Total 6, Failed 4, Errors 0, Passed 2, Ignored 6") 19 | } 20 | 21 | checkAfterTest := { 22 | val commandOutput = testCommandOutput(target.value, "test") 23 | // MyCompositeTest1 (includes 2x MyActualTests) + MyCompositeTest2 (includes 2x MyActualTests) + MyActualTests itself 24 | assertContainsLine(commandOutput, "[error] Failed: Total 15, Failed 10, Errors 0, Passed 5, Ignored 15") 25 | } 26 | 27 | def testCommandOutput(targetRoot: File, command: String): Seq[String] = { 28 | val raw = IO.readLines(targetRoot / "streams" / "test" / command / "_global" / "streams" / "out") 29 | raw.map(filterAnsi) 30 | } 31 | 32 | def assertContainsLine(output: Seq[String], expectedLine: String): Unit = { 33 | if (!output.contains(expectedLine)) { 34 | throw new AssertionError(s"!!!!! SBT output doesn't contain expected line: $expectedLine") 35 | } 36 | } 37 | 38 | def filterAnsi(text: String): String = 39 | text.replaceAll("\u001B\\[[\\d;]*[^\\d;]", "") 40 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-run-same-test-multiple-times/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.5.2 -------------------------------------------------------------------------------- /src/sbt-test/simple/test-run-same-test-multiple-times/src/test/scala/test.scala: -------------------------------------------------------------------------------- 1 | import org.junit._ 2 | import Assert._ 3 | 4 | import junit.framework.{TestCase, TestSuite} 5 | import org.junit.Assert.fail 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.Suite.SuiteClasses 8 | 9 | @RunWith(classOf[org.junit.runners.Suite]) 10 | @SuiteClasses(Array( 11 | classOf[MyActualTest], 12 | classOf[MyActualTest] 13 | )) 14 | class MyCompositeTest1 extends TestSuite 15 | 16 | @RunWith(classOf[org.junit.runners.Suite]) 17 | @SuiteClasses(Array( 18 | classOf[MyActualTest], 19 | classOf[MyActualTest] 20 | )) 21 | class MyCompositeTest2 extends TestSuite 22 | 23 | class MyActualTest { 24 | @Test def testSuccess1(): Unit = { println("print from testSuccess1") } 25 | 26 | @Test def testFailure1(): Unit = { println("print from testFailure1"); fail("fail reason") } 27 | @Test def testFailure2(): Unit = { println("print from testFailure2"); fail("fail reason") } 28 | 29 | @Test @Ignore def testIgnored1(): Unit = { println("print from testIgnored1"); fail("unreachable code") } 30 | @Test @Ignore def testIgnored2(): Unit = { println("print from testIgnored2"); fail("unreachable code") } 31 | @Test @Ignore def testIgnored3(): Unit = { println("print from testIgnored3"); fail("unreachable code") } 32 | } 33 | -------------------------------------------------------------------------------- /src/sbt-test/simple/test-run-same-test-multiple-times/test: -------------------------------------------------------------------------------- 1 | -> testOnly MyCompositeTest1 2 | > checkAfterTestOnly 3 | -> test 4 | > checkAfterTest -------------------------------------------------------------------------------- /src/sbt-test/simple/tests-run-once/build.sbt: -------------------------------------------------------------------------------- 1 | name := """tests-run-once""" 2 | 3 | scalaVersion := "2.10.7" 4 | 5 | libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" 6 | 7 | fork in Test := true 8 | 9 | testOptions += Tests.Argument(TestFrameworks.JUnit, "--verbosity=1") 10 | -------------------------------------------------------------------------------- /src/sbt-test/simple/tests-run-once/src/test/java/MyTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.Assert; 2 | import org.junit.Test; 3 | import org.junit.runner.RunWith; 4 | import org.junit.runners.Suite; 5 | import org.junit.runners.BlockJUnit4ClassRunner; 6 | 7 | 8 | public class MyTest extends SuperclassTests { 9 | private static int testRuns = 0; 10 | 11 | @Test 12 | public void simpleCheck() { 13 | System.out.println("hello" + testRuns); 14 | testRuns++; 15 | Assert.assertEquals(1, testRuns); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/sbt-test/simple/tests-run-once/src/test/java/SuperclassTests.java: -------------------------------------------------------------------------------- 1 | import org.junit.Assert; 2 | import org.junit.Test; 3 | import org.junit.runner.RunWith; 4 | import org.junit.runners.Suite; 5 | import org.junit.runners.BlockJUnit4ClassRunner; 6 | 7 | @RunWith(BlockJUnit4ClassRunner.class) 8 | public abstract class SuperclassTests { 9 | } 10 | -------------------------------------------------------------------------------- /src/sbt-test/simple/tests-run-once/test: -------------------------------------------------------------------------------- 1 | # verify that tests run once 2 | > test --------------------------------------------------------------------------------