├── scott-gradle-plugin ├── .gitignore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── gradle-plugins │ │ │ └── hu.advancedweb.scott-gradle-plugin.properties │ │ └── java │ │ └── hu │ │ └── advancedweb │ │ └── scott │ │ ├── ScottPluginExtension.java │ │ └── ScottPlugin.java ├── build.gradle └── gradlew.bat ├── docs ├── architecture.png ├── scott-in-action.png ├── release_guide.md ├── architecture.xml ├── configuration.md └── manual_setup.md ├── scott-tests ├── readme.md ├── configuration-example-classes │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── scott │ │ │ └── examples │ │ │ ├── AnnotationA.java │ │ │ ├── AnnotationB.java │ │ │ ├── ClassA.java │ │ │ ├── otherpackage │ │ │ └── ClassC.java │ │ │ ├── somepackage │ │ │ └── ClassB.java │ │ │ ├── ClassWithUnhandledException.java │ │ │ ├── ClassWithFeatures.java │ │ │ ├── ClassWithTrys.java │ │ │ ├── ClassWithVaryingMethodSizes.java │ │ │ └── ClassWithLambdas.java │ ├── readme.md │ └── pom.xml ├── junit5-tests │ ├── readme.md │ ├── src │ │ └── test │ │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── scott │ │ │ ├── InterfaceTest.java │ │ │ ├── SimpleTest.java │ │ │ ├── Java16Test.java │ │ │ ├── InterfaceTestHelper.java │ │ │ ├── TestInfoTest.java │ │ │ ├── Java14Test.java │ │ │ ├── Java11Test.java │ │ │ ├── AssumptionsTest.java │ │ │ ├── Java13Test.java │ │ │ ├── helper │ │ │ └── TestHelper.java │ │ │ ├── AssertionsTest.java │ │ │ └── NestedClassTest.java │ └── pom.xml ├── junit4-tests │ ├── readme.md │ ├── src │ │ └── test │ │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── scott │ │ │ ├── StaticFieldRecordingTestHelperSuperClass.java │ │ │ ├── StaticFieldRecordingTestHelperInterface.java │ │ │ ├── StaticFieldRecordingTestHelper.java │ │ │ ├── ControlFlowTest.java │ │ │ ├── RuleInjectionTest.java │ │ │ ├── VariableStateLeakageTest.java │ │ │ ├── MethodNameAndClassTrackingTest.java │ │ │ ├── TryWithResourcesRecordingTest.java │ │ │ ├── TestHelper.java │ │ │ ├── runtime │ │ │ └── report │ │ │ │ ├── ScottReportTest.java │ │ │ │ └── FailureRendererTest.java │ │ │ ├── FieldRecordingTest.java │ │ │ ├── RecordIncMutationTest.java │ │ │ ├── FieldRecordingInitialValuesTest.java │ │ │ ├── JdkLibTest.java │ │ │ ├── LambdaRecordingTest.java │ │ │ ├── ExceptionTest.java │ │ │ └── StaticFieldRecordingTest.java │ └── pom.xml ├── configuration-based-tests │ ├── readme.md │ ├── src │ │ └── test │ │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── scott │ │ │ ├── helper │ │ │ ├── ByteArrayClassLoader.java │ │ │ ├── TestScottRuntimeVerifier.java │ │ │ ├── ClassFileStructurePrinter.java │ │ │ └── InstrumentedObject.java │ │ │ ├── MultiInstrumentationTest.java │ │ │ ├── UnhandledExceptionRecordingTest.java │ │ │ ├── DisabledFeaturesConfigTest.java │ │ │ ├── TryTest.java │ │ │ ├── ReturnRecordingTest.java │ │ │ └── AnnotationInclusionExclusionTest.java │ └── pom.xml └── pom.xml ├── scott-examples ├── junit4 │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── hu │ │ │ │ └── advancedweb │ │ │ │ └── example │ │ │ │ ├── FaultyAdder.java │ │ │ │ ├── UserService.java │ │ │ │ ├── Counter.java │ │ │ │ └── User.java │ │ └── test │ │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── example │ │ │ ├── CounterTest.java │ │ │ ├── UserTest.java │ │ │ ├── StringTest.java │ │ │ ├── ParameterizedTest.java │ │ │ ├── ListTest.java │ │ │ └── LambdaTest.java │ ├── build.gradle │ ├── pom.xml │ ├── gradlew.bat │ └── readme.md ├── junit5 │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── src │ │ └── test │ │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── scott │ │ │ ├── VarTest.java │ │ │ ├── NestedClassTest.java │ │ │ └── JUnit5DemoTest.java │ ├── build.gradle │ ├── pom.xml │ ├── gradlew.bat │ └── readme.md └── cucumber-io-cucumber │ ├── src │ ├── main │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── example │ │ │ ├── Operation.java │ │ │ └── Calculator.java │ └── test │ │ ├── resources │ │ └── feature │ │ │ └── calculator.feature │ │ └── java │ │ └── hu │ │ └── advancedweb │ │ └── example │ │ ├── FeatureTest.java │ │ └── step │ │ └── CalculatorSteps.java │ ├── readme.md │ └── pom.xml ├── .gitignore ├── scott ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── MANIFEST.MF │ │ └── java │ │ │ └── hu │ │ │ └── advancedweb │ │ │ └── scott │ │ │ ├── instrumentation │ │ │ ├── transformation │ │ │ │ ├── Logger.java │ │ │ │ ├── config │ │ │ │ │ └── ClassMatcher.java │ │ │ │ ├── LocalVariableScope.java │ │ │ │ ├── AccessedField.java │ │ │ │ ├── ConstructorTransformerMethodVisitor.java │ │ │ │ ├── VariableType.java │ │ │ │ ├── ScottClassTransformer.java │ │ │ │ ├── StateTrackingClassVisitor.java │ │ │ │ └── param │ │ │ │ │ ├── DiscoveryClassVisitor.java │ │ │ │ │ └── DiscoveryMethodVisitor.java │ │ │ ├── ScottAgent.java │ │ │ ├── ScottClassFileTransformer.java │ │ │ └── ScottConfigurer.java │ │ │ └── runtime │ │ │ ├── report │ │ │ ├── Snapshot.java │ │ │ ├── javasource │ │ │ │ ├── SourcePathResolver.java │ │ │ │ ├── MethodBoundaryExtractor.java │ │ │ │ └── MethodSource.java │ │ │ └── ScottReport.java │ │ │ ├── track │ │ │ ├── StateData.java │ │ │ └── ContextualData.java │ │ │ ├── ExceptionUtil.java │ │ │ ├── ScottJUnit5Extension.java │ │ │ └── ScottReportingRule.java │ └── test │ │ └── java │ │ └── hu │ │ └── advancedweb │ │ └── scott │ │ └── instrumentation │ │ └── transformation │ │ ├── VariableTypeTest.java │ │ └── config │ │ └── ClassMatcherTest.java └── readme.md ├── scott-maven-plugin ├── META-INF │ └── m2e │ │ └── lifecycle-mapping-metadata.xml ├── src │ └── hu │ │ └── advancedweb │ │ └── maven │ │ └── ScottAgentMojo.java └── pom.xml ├── pom.xml ├── LICENSE ├── azure-pipelines.yml └── CONTRIBUTING.md /scott-gradle-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodie/scott/HEAD/docs/architecture.png -------------------------------------------------------------------------------- /docs/scott-in-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodie/scott/HEAD/docs/scott-in-action.png -------------------------------------------------------------------------------- /scott-gradle-plugin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodie/scott/HEAD/scott-gradle-plugin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scott-tests/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - Tests 2 | =========================== 3 | 4 | These projects ensure that Scott remains bug free. 5 | 6 | -------------------------------------------------------------------------------- /scott-examples/junit4/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodie/scott/HEAD/scott-examples/junit4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scott-examples/junit5/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodie/scott/HEAD/scott-examples/junit5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scott-gradle-plugin/src/main/resources/META-INF/gradle-plugins/hu.advancedweb.scott-gradle-plugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=hu.advancedweb.scott.ScottPlugin 2 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/AnnotationA.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | public @interface AnnotationA { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/AnnotationB.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | public @interface AnnotationB { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | target 4 | .classpath 5 | .project 6 | .settings 7 | pom.xml.asc 8 | dependency-reduced-pom.xml 9 | out 10 | .shelf 11 | .idea 12 | *.iml 13 | *.ipr 14 | *.iws 15 | -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/src/main/java/hu/advancedweb/example/Operation.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | public interface Operation { 4 | 5 | int perform(int arg1, int arg2); 6 | 7 | } -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/src/test/resources/feature/calculator.feature: -------------------------------------------------------------------------------- 1 | Feature: Calculator 2 | 3 | Scenario: Addition 4 | Given a Calculator 5 | When I add 1 and 2 6 | Then the result is 4 7 | 8 | -------------------------------------------------------------------------------- /scott/src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Premain-Class: hu.advancedweb.scott.instrumentation.ScottAgent 3 | Can-Redefine-Classes: true 4 | Can-Retransform-Classes: true 5 | Can-Set-Native-Method-Prefix: true -------------------------------------------------------------------------------- /scott-tests/junit5-tests/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - JUnit 5 Test Suite 2 | ======================================== 3 | 4 | Contains narrow-scoped tests to test Scott Test Reporter in integration with JUnit 5, Maven and Java 14. 5 | 6 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/ClassA.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | @AnnotationA 4 | public class ClassA { 5 | 6 | public String hello() { 7 | return "Hello"; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /scott-gradle-plugin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /scott-examples/junit4/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /scott-examples/junit5/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - JUnit 4 Test Suite 2 | ======================================== 3 | 4 | This is the core test suite for Scott, containing most of the feature tests. 5 | 6 | It tests Scott Test Reporter in integration with JUnit 4 and Java 8 in Maven build using Scott Agent. 7 | 8 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/main/java/hu/advancedweb/example/FaultyAdder.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | public class FaultyAdder { 4 | 5 | public static int add(int a, int b) { 6 | if (a == 2 && b == 2) { 7 | return 5; // Yuck! A bug! 8 | } else { 9 | return a + b; 10 | } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - Example classes for configuration based tests 2 | =================================================================== 3 | 4 | This package contains example classes that can be used during configuration based tests. 5 | See `configuration-based-tests` module. 6 | 7 | -------------------------------------------------------------------------------- /scott/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter 2 | =================== 3 | 4 | This project contains the instrumentation and runtime parts of Scott. 5 | 6 | 7 | Build 8 | ----- 9 | ``` mvn install ``` 10 | 11 | For a Docker-based setup, see the [development guide](https://github.com/dodie/scott/blob/master/docs/development-guide.md). 12 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/StaticFieldRecordingTestHelperSuperClass.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | public class StaticFieldRecordingTestHelperSuperClass { 4 | public static String SOME_VALUE_FROM_SUPER_TO_READ = "super"; 5 | public static String SOME_VALUE_FROM_SUPER_TO_WRITE = "before_write"; 6 | } 7 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/otherpackage/ClassC.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples.otherpackage; 2 | 3 | import hu.advancedweb.scott.examples.AnnotationB; 4 | 5 | @AnnotationB 6 | public class ClassC { 7 | 8 | public String hello() { 9 | return "Hello"; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/somepackage/ClassB.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples.somepackage; 2 | 3 | import hu.advancedweb.scott.examples.AnnotationA; 4 | 5 | @AnnotationA 6 | public class ClassB { 7 | 8 | public String hello() { 9 | return "Hello"; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/InterfaceTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | public class InterfaceTest { 7 | 8 | @Test 9 | public void test() throws Exception { 10 | assertEquals(2, 2); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scott-examples/junit4/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id "hu.advancedweb.scott-gradle-plugin" version "4.0.1" 4 | } 5 | 6 | group 'hu.advancedweb' 7 | version '1.0-SNAPSHOT' 8 | 9 | sourceCompatibility = 1.8 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | testCompile group: 'junit', name: 'junit', version: '4.12' 17 | } 18 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/main/java/hu/advancedweb/example/UserService.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | public class UserService { 4 | 5 | private Long lastId = 41L; 6 | 7 | public User createUser(String email, String name) { 8 | User user; 9 | user = new User(); 10 | user.setId(++lastId); 11 | user.setEmail(email); 12 | user.setName(name); 13 | return user; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/Logger.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | class Logger { 4 | 5 | private boolean enabled; 6 | 7 | Logger(boolean enabled) { 8 | this.enabled = enabled; 9 | } 10 | 11 | void log(String message) { 12 | if (enabled) { 13 | System.out.println("Scott instrumentation: " + message); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /scott-maven-plugin/META-INF/m2e/lifecycle-mapping-metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | prepare-agent 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/report/Snapshot.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.report; 2 | 3 | /** 4 | * Represents a variable name - value pair of a tracked data. 5 | * 6 | * @author David Csakvari 7 | */ 8 | class Snapshot { 9 | 10 | final String name; 11 | final String value; 12 | 13 | public Snapshot(String name, String value) { 14 | this.name = name; 15 | this.value = value; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/main/java/hu/advancedweb/example/Counter.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | public class Counter { 4 | 5 | int state; 6 | 7 | public int get() { 8 | return state; 9 | } 10 | 11 | public void increase() { 12 | state++; 13 | } 14 | 15 | public void decrease() { 16 | state--; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "Counter [state=" + state + "]"; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/StaticFieldRecordingTestHelperInterface.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | public interface StaticFieldRecordingTestHelperInterface { 4 | 5 | /* 6 | * Scott can't track constants (public static final), because they get inlined at compile time. 7 | * So we don't have any test for the following field. 8 | */ 9 | 10 | public static String SOME_VALUE_FROM_INTERFACE = "interface"; 11 | } 12 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/test/java/hu/advancedweb/example/CounterTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class CounterTest { 8 | 9 | @Test 10 | public void test_1() { 11 | Counter counter = new Counter(); 12 | 13 | counter.increase(); 14 | counter.increase(); 15 | 16 | int state = counter.get(); 17 | 18 | assertEquals(state, 3); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/StaticFieldRecordingTestHelper.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | public class StaticFieldRecordingTestHelper { 4 | 5 | /* 6 | * Scott can't track constants (public static final), because they get inlined at compile time. 7 | * This is the reason of the SOME_VALUE_TO_READ field not being final. 8 | */ 9 | 10 | public static String SOME_VALUE_TO_READ = "42"; 11 | public static String SOME_VALUE_TO_WRITE = "before_write"; 12 | } 13 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/ClassWithUnhandledException.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | import java.util.function.Consumer; 4 | 5 | 6 | public class ClassWithUnhandledException { 7 | 8 | public void boom() { 9 | Consumer unsafeConsumer = (s) -> { 10 | if (s == null) { 11 | throw new RuntimeException("Something went wrong."); 12 | } 13 | s.length(); 14 | }; 15 | unsafeConsumer.accept(null); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/test/java/hu/advancedweb/example/UserTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class UserTest { 8 | 9 | @Test 10 | public void test_1() { 11 | UserService service = new UserService(); 12 | 13 | User john = service.createUser("john@doe", "John Doe"); 14 | User jane = service.createUser("jane@doe", "Jane Doe"); 15 | 16 | assertEquals(john.getId(), jane.getId()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /scott-examples/junit5/src/test/java/hu/advancedweb/scott/VarTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class VarTest { 8 | 9 | @Test 10 | public void testWithMessageSupplier() { 11 | var first = "Hello"; 12 | var last = "World"; 13 | 14 | var concatenated = first + " " + last; 15 | 16 | assertEquals("Goodbye World", concatenated, 17 | () -> "Incorrect message."); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/SimpleTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import hu.advancedweb.scott.helper.TestHelper; 9 | 10 | public class SimpleTest { 11 | 12 | @Test 13 | void test() { 14 | String dot = "."; 15 | assertEquals(wrapped(dot), TestHelper.getLastRecordedStateForVariable("dot")); 16 | assertEquals(".", dot); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/src/test/java/hu/advancedweb/example/FeatureTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import org.junit.runner.RunWith; 4 | 5 | import io.cucumber.junit.CucumberOptions; 6 | import io.cucumber.junit.Cucumber; 7 | 8 | /** 9 | * Example configuration to include Scott report in Cucumber's output. 10 | */ 11 | @RunWith(Cucumber.class) 12 | @CucumberOptions( 13 | plugin = { 14 | "hu.advancedweb.scott.runtime.ScottCucumberIoFormatter:target/mycuc.html" 15 | }, 16 | features = {"src/test/resources/feature/"}) 17 | public class FeatureTest {} 18 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/Java16Test.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import hu.advancedweb.scott.helper.TestHelper; 7 | import org.junit.jupiter.api.Test; 8 | 9 | 10 | public class Java16Test { 11 | 12 | @Test 13 | public void testWithRecord() { 14 | 15 | record X(String y) { } 16 | 17 | var x = new X("hello"); 18 | 19 | var y = x.y(); 20 | 21 | assertEquals(wrapped("hello"), TestHelper.getLastRecordedStateForVariable("y")); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/ControlFlowTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.hamcrest.CoreMatchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import org.junit.Test; 7 | 8 | public class ControlFlowTest { 9 | 10 | @Test 11 | public void for_test() throws Exception { 12 | for (int i = 0; i < 10; i++) { 13 | int j = i * 2; 14 | assertThat(TestHelper.getLastRecordedStateForVariable("i"), equalTo(Integer.toString(i))); 15 | assertThat(TestHelper.getLastRecordedStateForVariable("j"), equalTo(Integer.toString(j))); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/ClassWithFeatures.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | import java.util.function.Function; 4 | 5 | public class ClassWithFeatures { 6 | 7 | private static Integer I = 1; 8 | private static Integer i = 1; 9 | 10 | public static Integer i(int j) { 11 | return j+1; 12 | } 13 | 14 | public Integer ii() { 15 | int lo = i(4); 16 | lo++; 17 | I++; 18 | i++; 19 | 20 | Function f = (x) -> x * x; 21 | 22 | return f.apply(1 + I + i + lo + iii(2)); 23 | } 24 | 25 | private int iii(int j) { 26 | return j * j; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | hu.advancedweb 7 | scott-parent 8 | 4.0.1 9 | pom 10 | 11 | Scott Test Reporter - POM 12 | 13 | 14 | scott 15 | scott-maven-plugin 16 | scott-tests 17 | 18 | 19 | -------------------------------------------------------------------------------- /scott-examples/junit5/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id "hu.advancedweb.scott-gradle-plugin" version "4.0.1" 4 | } 5 | 6 | group 'hu.advancedweb' 7 | version '1.0-SNAPSHOT' 8 | 9 | sourceCompatibility = 1.8 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | testCompile('org.junit.jupiter:junit-jupiter-api:5.3.1') 17 | testCompile('org.junit.jupiter:junit-jupiter-params:5.3.1') 18 | testRuntime('org.junit.jupiter:junit-jupiter-engine:5.3.1') 19 | testRuntime('org.mockito:mockito-core:2.2.28') 20 | } 21 | 22 | 23 | test { 24 | useJUnitPlatform() 25 | testLogging { 26 | events "passed", "skipped", "failed" 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/InterfaceTestHelper.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import org.junit.jupiter.api.AfterAll; 4 | import org.junit.jupiter.api.AfterEach; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.TestInfo; 8 | 9 | public interface InterfaceTestHelper { 10 | 11 | @BeforeAll 12 | static void beforeAllTests() { 13 | } 14 | 15 | @AfterAll 16 | static void afterAllTests() { 17 | } 18 | 19 | @BeforeEach 20 | default void beforeEachTest(TestInfo testInfo) { 21 | } 22 | 23 | @AfterEach 24 | default void afterEachTest(TestInfo testInfo) { 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/track/StateData.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.track; 2 | 3 | public class StateData extends ContextualData { 4 | 5 | /** Name of the variable or field. */ 6 | public final String name; 7 | 8 | /** Recorded value. */ 9 | public final String value; 10 | 11 | public StateData(int lineNumber, String methodName, String name, String value) { 12 | super(lineNumber, methodName); 13 | this.name = name; 14 | this.value = value; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "StateData [lineNumber=" + lineNumber + ", methodName=" + methodName + ", name=" + name + ", value=" + value + "]"; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - Configuration based tests 2 | =============================================== 3 | 4 | This test suite excersizes Scott via its Java API by dynamically loading 5 | class files and transforming them on the fly with different configuration. 6 | 7 | This module also excersizes behavior not used directly by the Scott Test Reporter, 8 | for example excluding short methods from the instrumentation. 9 | 10 | It uses class files from the `configuration-example-classes` module by file path based lookup 11 | rather than an ususal classpath based dependency. If the example classes were on the 12 | classpath the test mechanism could not load them dynamically. 13 | -------------------------------------------------------------------------------- /scott/src/test/java/hu/advancedweb/scott/instrumentation/transformation/VariableTypeTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class VariableTypeTest { 8 | 9 | @Test 10 | public void test() { 11 | assertEquals(VariableType.INTEGER, VariableType.getReturnTypeFromMethodDesc("()I")); 12 | assertEquals(VariableType.REFERENCE, VariableType.getReturnTypeFromMethodDesc("()Ljava/lang/String")); 13 | assertEquals(VariableType.VOID, VariableType.getReturnTypeFromMethodDesc("()V")); 14 | assertEquals(VariableType.DOUBLE, VariableType.getReturnTypeFromMethodDesc("(I)D")); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /scott-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | hu.advancedweb 7 | scott-tests 8 | pom 9 | 4.0.1 10 | 11 | Scott Test Reporter - Tests 12 | 13 | 14 | junit4-tests 15 | junit5-tests 16 | configuration-example-classes 17 | configuration-based-tests 18 | 19 | 20 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/helper/ByteArrayClassLoader.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.helper; 2 | 3 | import java.util.Map; 4 | 5 | public class ByteArrayClassLoader extends ClassLoader { 6 | 7 | private Map classBytecodes; 8 | 9 | public ByteArrayClassLoader(ClassLoader parent, Map classBytecodes) { 10 | super(parent); 11 | this.classBytecodes = classBytecodes; 12 | } 13 | 14 | @Override 15 | public Class findClass(String name) { 16 | if (!classBytecodes.containsKey(name)) { 17 | throw new RuntimeException("Class not found: " + name); 18 | } 19 | byte[] bytes = classBytecodes.get(name); 20 | return defineClass(name, bytes, 0, bytes.length); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/track/ContextualData.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.track; 2 | 3 | /** 4 | * Represents a data point collected at a given line number. 5 | * 6 | * @author David Csakvari 7 | */ 8 | public class ContextualData { 9 | 10 | /** Line number where the data is collected. */ 11 | public final int lineNumber; 12 | 13 | /** Method where the data is collected. */ 14 | public final String methodName; 15 | 16 | public ContextualData(int lineNumber, String methodName) { 17 | this.lineNumber = lineNumber; 18 | this.methodName = methodName; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "ContextualData [lineNumber=" + lineNumber + ", methodName=" + methodName + "]"; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/main/java/hu/advancedweb/example/User.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | public class User { 4 | 5 | private Long id; 6 | 7 | private String email; 8 | 9 | private String name; 10 | 11 | public Long getId() { 12 | return id; 13 | } 14 | 15 | public void setId(Long id) { 16 | this.id = id; 17 | } 18 | 19 | public String getEmail() { 20 | return email; 21 | } 22 | 23 | public void setEmail(String email) { 24 | this.email = email; 25 | } 26 | 27 | public String getFirstName() { 28 | return name; 29 | } 30 | 31 | public void setName(String name) { 32 | this.name = name; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "User [id=" + id + ", email=" + email + ", name=" + name + "]"; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/ClassWithTrys.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.PrintStream; 6 | 7 | public class ClassWithTrys { 8 | 9 | public boolean hello() { 10 | try { 11 | if (this.getClass().getName().isEmpty()) { 12 | return false; 13 | } 14 | 15 | String string = "a"; 16 | return true; 17 | } finally { 18 | System.out.println("Hey"); 19 | } 20 | } 21 | 22 | public void hello2() throws IOException { 23 | try (ByteArrayOutputStream out = new ByteArrayOutputStream(); 24 | PrintStream printStream = new PrintStream(out)) { 25 | 26 | printStream.append("Hello World"); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/RuleInjectionTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.hamcrest.CoreMatchers.notNullValue; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.Field; 9 | 10 | import org.junit.Test; 11 | 12 | public class RuleInjectionTest { 13 | 14 | @Test 15 | public void ruleInjected() throws Exception { 16 | Field field = this.getClass().getDeclaredField("scottReportingRule"); 17 | 18 | Annotation[] annotations = field.getDeclaredAnnotations(); 19 | Object value = field.get(this); 20 | 21 | assertThat(value, notNullValue()); 22 | assertTrue(annotations.length != 0); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/TestInfoTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Tag; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.TestInfo; 10 | 11 | import hu.advancedweb.scott.helper.TestHelper; 12 | 13 | public class TestInfoTest { 14 | 15 | @Test 16 | @DisplayName("TEST 1") 17 | @Tag("my-tag") 18 | void test1(TestInfo testInfo) { 19 | assertEquals("TEST 1", testInfo.getDisplayName()); 20 | assertTrue(testInfo.getTags().contains("my-tag")); 21 | assertTrue(TestHelper.getLastRecordedStateForVariable("testInfo") != null); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/ExceptionUtil.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | class ExceptionUtil { 6 | 7 | private ExceptionUtil() { 8 | // Utility class, use static methods. 9 | } 10 | 11 | static void setExceptionMessage(Object object, Object fieldValue) { 12 | final String fieldName = "detailMessage"; 13 | Class clazz = object.getClass(); 14 | while (clazz != null) { 15 | try { 16 | Field field = clazz.getDeclaredField(fieldName); 17 | field.setAccessible(true); 18 | field.set(object, fieldValue); 19 | return; 20 | } catch (NoSuchFieldException e) { 21 | clazz = clazz.getSuperclass(); 22 | } catch (Exception e) { 23 | throw new IllegalStateException(e); 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/Java14Test.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import hu.advancedweb.scott.helper.TestHelper; 7 | import org.junit.jupiter.api.Test; 8 | 9 | 10 | public class Java14Test { 11 | 12 | @Test 13 | public void testWithTextBlock() { 14 | Object o = "hello"; 15 | 16 | final String result; 17 | if (o instanceof String s) { 18 | result = s + " world"; 19 | } else { 20 | result = "not a string"; 21 | } 22 | 23 | assertEquals(wrapped("hello"), TestHelper.getLastRecordedStateForVariable("o")); 24 | assertEquals(wrapped("hello"), TestHelper.getLastRecordedStateForVariable("s")); 25 | assertEquals("hello world", result); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/VariableStateLeakageTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import org.junit.FixMethodOrder; 6 | import org.junit.Test; 7 | import org.junit.runners.MethodSorters; 8 | 9 | import hu.advancedweb.scott.runtime.track.StateRegistry; 10 | 11 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 12 | public class VariableStateLeakageTest { 13 | 14 | @Test 15 | public void step_1_run_a_test_and_check_that_scott_recorded_something() throws Exception { 16 | int i = 5; 17 | i = i + 2; 18 | assertTrue(!StateRegistry.getLocalVariableStates().isEmpty()); 19 | } 20 | 21 | @Test 22 | public void step_2_check_that_the_recorded_variable_states_are_cleared_for_the_next_test() throws Exception { 23 | assertTrue(StateRegistry.getLocalVariableStates().isEmpty()); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/MethodNameAndClassTrackingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.hamcrest.CoreMatchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import org.junit.Test; 7 | 8 | import hu.advancedweb.scott.runtime.track.StateRegistry; 9 | 10 | public class MethodNameAndClassTrackingTest { 11 | 12 | @Test 13 | public void test1() throws Exception { 14 | assertThat(StateRegistry.getTestClassType(), equalTo("hu/advancedweb/scott/MethodNameAndClassTrackingTest")); 15 | assertThat(StateRegistry.getTestMethodName(), equalTo("test1")); 16 | } 17 | 18 | @Test 19 | public void test2() throws Exception { 20 | assertThat(StateRegistry.getTestClassType(), equalTo("hu/advancedweb/scott/MethodNameAndClassTrackingTest")); 21 | assertThat(StateRegistry.getTestMethodName(), equalTo("test2")); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /scott-examples/junit5/src/test/java/hu/advancedweb/scott/NestedClassTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Nested; 7 | import org.junit.jupiter.api.Test; 8 | 9 | 10 | public class NestedClassTest { 11 | 12 | String value; 13 | 14 | @BeforeEach 15 | void reset() { 16 | value = "1"; 17 | } 18 | 19 | @Test 20 | void test() { 21 | String dot = "."; 22 | value += dot; 23 | assertEquals("1", value); 24 | } 25 | 26 | @Nested 27 | class NestedClass { 28 | 29 | String nestedValue = "a"; 30 | 31 | @BeforeEach 32 | void reset() { 33 | value = value + "2"; 34 | } 35 | 36 | @Test 37 | void test() { 38 | String dot = "."; 39 | nestedValue += dot; 40 | value += dot + nestedValue; 41 | 42 | assertEquals("12.a", value); 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /scott-examples/junit4/src/test/java/hu/advancedweb/example/StringTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class StringTest { 8 | 9 | @Test 10 | public void test_1() { 11 | String first = "Hello"; 12 | String last = "World"; 13 | 14 | String concatenated = first + " " + last; 15 | 16 | assertEquals("Goodbye World", concatenated); 17 | } 18 | 19 | @Test 20 | public void test_2() { 21 | String hello = "Hello World"; 22 | 23 | int indexOfSpace = hello.indexOf(" "); 24 | String lastPart = hello.substring(indexOfSpace); 25 | 26 | assertEquals("World", lastPart); 27 | } 28 | 29 | @Test 30 | public void test_3() { 31 | String empty = ""; 32 | String first = "1"; 33 | String last = "2"; 34 | 35 | String concatenated = empty + first + last; 36 | 37 | assertEquals(12, concatenated); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | configuration-example-classes 6 | 4.0.1 7 | 8 | UTF-8 9 | 10 | Scott Test Reporter - Exampe classes for configuration-based tests 11 | 12 | 13 | 14 | 15 | org.apache.maven.plugins 16 | maven-compiler-plugin 17 | 3.8.1 18 | 19 | 1.8 20 | 1.8 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/Java11Test.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import hu.advancedweb.scott.helper.TestHelper; 7 | import org.junit.jupiter.api.Test; 8 | import java.util.function.Function; 9 | 10 | 11 | public class Java11Test { 12 | 13 | @Test 14 | void varTest() { 15 | var dot = "."; 16 | assertEquals(wrapped(dot), TestHelper.getLastRecordedStateForVariable("dot")); 17 | assertEquals(".", dot); 18 | } 19 | 20 | @Test 21 | public void lambdaVarTest() throws Exception { 22 | Function lambda = (var a) -> { 23 | assertEquals(wrapped(a), TestHelper.getLastRecordedStateForVariable("a")); 24 | return a; 25 | }; 26 | String result = lambda.apply("1"); 27 | assertEquals(wrapped(result), TestHelper.getLastRecordedStateForVariable("result")); 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/src/test/java/hu/advancedweb/example/step/CalculatorSteps.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example.step; 2 | 3 | import static org.hamcrest.Matchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import io.cucumber.java.en.Given; 7 | import io.cucumber.java.en.Then; 8 | import io.cucumber.java.en.When; 9 | import hu.advancedweb.example.Calculator; 10 | 11 | public class CalculatorSteps { 12 | 13 | public static Calculator calculator; 14 | 15 | @Given("^a Calculator") 16 | public void a_calculator() { 17 | calculator = new Calculator(); 18 | } 19 | 20 | @When("^I add (\\d+) and (\\d+)$") 21 | public void i_add_two_numbers(String arg1, String arg2) { 22 | calculator.push(arg1); 23 | calculator.push(arg2); 24 | calculator.push("+"); 25 | } 26 | 27 | @Then("^the result is (\\d+)$") 28 | public void the_result_is(String expected) { 29 | String result = calculator.evaluate(); 30 | assertThat(result, equalTo(expected)); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/ClassWithVaryingMethodSizes.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | /** 4 | * Note that this file is sensitive to formatting. 5 | * 6 | * @author David Csakvari 7 | */ 8 | public class ClassWithVaryingMethodSizes { 9 | 10 | public String methodWith1LineBodyInlineDeclaration() { return "Hello from methodWith1LineBodyInlineDeclaration"; } 11 | 12 | public String methodWith1LineBody() { 13 | return "Hello from methodWith1LineBody"; 14 | } 15 | 16 | public String methodWith3LineBody() { 17 | StringBuilder sb = new StringBuilder(); 18 | sb.append("Hello from mediumMethod"); 19 | return sb.toString(); 20 | } 21 | 22 | public String methodWith9LineBody() { 23 | StringBuilder sb = new StringBuilder(); 24 | sb.append("H"); 25 | sb.append("e"); 26 | sb.append("l"); 27 | sb.append("l"); 28 | sb.append("o"); 29 | sb.append(" "); 30 | sb.append("from longMethod"); 31 | return sb.toString(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/src/main/java/hu/advancedweb/example/Calculator.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import java.util.Deque; 4 | import java.util.LinkedList; 5 | 6 | public class Calculator { 7 | 8 | private final Deque stack = new LinkedList(); 9 | 10 | public void push(String arg) { 11 | stack.add(arg); 12 | } 13 | 14 | public String evaluate() { 15 | 16 | String op = stack.removeLast(); 17 | Integer x = Integer.parseInt(stack.removeLast()); 18 | Integer y = Integer.parseInt(stack.removeLast()); 19 | 20 | Integer val; 21 | if (op.equals("-")) { 22 | val = x - y; 23 | } else if (op.equals("+")) { 24 | val = x + y; 25 | } else if (op.equals("*")) { 26 | val = x * y; 27 | } else if (op.equals("/")) { 28 | val = x / y; 29 | } else { 30 | throw new IllegalArgumentException(); 31 | } 32 | 33 | stack.push(Integer.toString(val)); 34 | 35 | return stack.getLast(); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "[stack=" + stack + "]"; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/helper/TestScottRuntimeVerifier.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.helper; 2 | 3 | public interface TestScottRuntimeVerifier { 4 | 5 | public void trackMethodStart(int lineNumber, String methodName, Class clazz); 6 | 7 | public void trackEndOfArgumentsAtMethodStart(int lineNumber, String methodName, Class clazz); 8 | 9 | public void trackLocalVariableState(Object value, String name, int lineNumber, String methodName, Class clazz); 10 | 11 | public void trackUnhandledException(Throwable throwable, int lineNumber, String methodName, Class clazz); 12 | 13 | public void trackReturn(int lineNumber, String methodName, Class clazz); 14 | 15 | public void trackReturn(Object value, int lineNumber, String methodName, Class clazz); 16 | 17 | public void trackLambdaDefinition(int lineNumber, String methodName, Class clazz); 18 | 19 | public void trackFieldState(Object value, String name, int lineNumber, String methodName, Class clazz, boolean isStatic, String owner); 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 David Csakvari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/test/java/hu/advancedweb/example/ParameterizedTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import static org.hamcrest.CoreMatchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.junit.runners.Parameterized; 12 | import org.junit.runners.Parameterized.Parameters; 13 | 14 | @RunWith(Parameterized.class) 15 | public class ParameterizedTest { 16 | 17 | @Parameters 18 | public static Collection data() { 19 | return Arrays.asList(new Object[][] { 20 | { 0, 0, 0 }, 21 | { 1, 1, 2 }, 22 | { 2, 1, 3 }, 23 | { 2, 2, 4 } 24 | }); 25 | } 26 | 27 | private final int a; 28 | private final int b; 29 | private final int expectedSum; 30 | 31 | public ParameterizedTest(int a, int b, int expectedSum) { 32 | this.a = a; 33 | this.b = b; 34 | this.expectedSum = expectedSum; 35 | } 36 | 37 | @Test 38 | public void testAddition() { 39 | int sum = FaultyAdder.add(a, b); 40 | assertThat(sum, equalTo(expectedSum)); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/ScottJUnit5Extension.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime; 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext; 4 | import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; 5 | 6 | import hu.advancedweb.scott.runtime.report.FailureRenderer; 7 | 8 | public class ScottJUnit5Extension implements TestExecutionExceptionHandler { 9 | 10 | @Override 11 | public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { 12 | String testClassName = context.getTestClass().isPresent() ? context.getTestClass().get().getTypeName() : null; 13 | String testMethodName = context.getTestMethod().isPresent() ? context.getTestMethod().get().getName() : null; 14 | 15 | if (throwable instanceof AssertionError) { 16 | throw new AssertionError(FailureRenderer.render(testClassName, testMethodName, throwable), throwable); 17 | } else if (throwable instanceof Error) { 18 | throw new Error(FailureRenderer.render(testClassName, testMethodName, throwable), throwable); 19 | } else { 20 | throw new Throwable(FailureRenderer.render(testClassName, testMethodName, throwable), throwable); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/TryWithResourcesRecordingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.PrintStream; 8 | import java.io.StringWriter; 9 | 10 | import static org.hamcrest.CoreMatchers.equalTo; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | 13 | public class TryWithResourcesRecordingTest { 14 | 15 | @Test 16 | public void try_with_resources_one_resource() throws IOException { 17 | try (StringWriter out = new StringWriter()) { 18 | 19 | out.write("Hello World"); 20 | 21 | assertThat(TestHelper.getLastRecordedStateForVariable("out"), equalTo(out.toString())); 22 | } 23 | } 24 | 25 | @Test 26 | public void try_with_resources_two_resources() throws IOException { 27 | try (ByteArrayOutputStream out = new ByteArrayOutputStream(); 28 | PrintStream printStream = new PrintStream(out)) { 29 | 30 | printStream.append("Hello World"); 31 | 32 | assertThat(TestHelper.getLastRecordedStateForVariable("out"), equalTo(out.toString())); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/AssumptionsTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assumptions.assumeTrue; 6 | import static org.junit.jupiter.api.Assumptions.assumingThat; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import hu.advancedweb.scott.helper.TestHelper; 11 | 12 | public class AssumptionsTest { 13 | 14 | @Test 15 | void testOnlyOnCiServer() { 16 | assumeTrue("CI".equals(env())); 17 | String s = "value"; 18 | assertEquals("value", s); 19 | assertEquals(wrapped("value"), TestHelper.getLastRecordedStateForVariable("s")); 20 | } 21 | 22 | @Test 23 | void testInAllEnvironments() { 24 | assumingThat("CI".equals(env()), () -> { 25 | String s = "value"; 26 | assertEquals("value", s); 27 | assertEquals(wrapped("value"), TestHelper.getLastRecordedStateForVariable("s")); 28 | }); 29 | 30 | String s = "value"; 31 | assertEquals("value", s); 32 | assertEquals(wrapped("value"), TestHelper.getLastRecordedStateForVariable("s")); 33 | } 34 | 35 | public String env() { 36 | return "CI"; 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /scott-gradle-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | id 'java-gradle-plugin' 4 | id 'com.gradle.plugin-publish' version '0.16.0' 5 | } 6 | 7 | group 'hu.advanceweb' 8 | version '4.0.1' 9 | 10 | sourceCompatibility = 1.8 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | pluginBundle { 17 | website = 'https://github.com/dodie/scott' 18 | vcsUrl = 'https://github.com/dodie/scott.git' 19 | tags = ['testing-tools','java', 'junit', 'assertions', 'scott'] 20 | } 21 | 22 | gradlePlugin { 23 | plugins { 24 | scottPlugin { 25 | id = 'hu.advancedweb.scott-gradle-plugin' 26 | implementationClass = 'hu.advancedweb.scott.ScottPlugin' 27 | displayName = 'Detailed failure reports and hassle free assertions for Java tests' 28 | description = 'Scott provides detailed failure messages for tests written in Java, without the use of complex assertion libraries to aid developers in rapid development, troubleshooting and debugging of tests. All information is presented on the source code of the test method as comments.' 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | compile gradleApi() 35 | compile 'org.codehaus.groovy:groovy-all:2.3.11' 36 | } 37 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/config/ClassMatcher.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation.config; 2 | 3 | import java.util.List; 4 | 5 | class ClassMatcher { 6 | 7 | private ClassMatcher() { 8 | // Utility class, use static methods instead of instantiating this class. 9 | } 10 | 11 | static boolean anyMatchesAsClassOrPackage(List classFqns, List fqns) { 12 | for (String classFqn : classFqns) { 13 | if (matchesAsClass(classFqn, fqns) || matchesAsInnerClass(classFqn, fqns) || matchesAsPackage(classFqn, fqns)) { 14 | return true; 15 | } 16 | } 17 | 18 | return false; 19 | } 20 | 21 | static boolean matchesAsClass(String classFqn, List fqns) { 22 | return fqns.contains(classFqn); 23 | } 24 | 25 | static boolean matchesAsInnerClass(String classFqn, List fqns) { 26 | for (String fqn : fqns) { 27 | if (classFqn.startsWith(fqn + "$")) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | } 34 | 35 | static boolean matchesAsPackage(String classFqn, List fqns) { 36 | for (String fqn : fqns) { 37 | if (classFqn.startsWith(fqn + ".")) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/LocalVariableScope.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Metadata of a local variable declared in a test case. 7 | * 8 | * @author David Csakvari 9 | */ 10 | class LocalVariableScope { 11 | 12 | final int var; 13 | final String name; 14 | final VariableType variableType; 15 | final int start; 16 | final int end; 17 | final int startIndex; 18 | final int endIndex; 19 | final List additionalIndexes; 20 | 21 | LocalVariableScope(int var, String name, VariableType variableType, int start, int end, int startIndex, int endIndex, List additionalIndexes) { 22 | this.var = var; 23 | this.name = name; 24 | this.variableType = variableType; 25 | this.start = start; 26 | this.end = end; 27 | this.startIndex = startIndex; 28 | this.endIndex = endIndex; 29 | this.additionalIndexes = additionalIndexes; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "LocalVariableScope [var=" + var + ", name=" + name + ", variableType=" + variableType + ", start=" + start + ", end=" + end + ", startIndex=" + startIndex + ", endIndex=" + endIndex + ", additionalIndexes=" + additionalIndexes + "]"; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/Java13Test.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import hu.advancedweb.scott.helper.TestHelper; 7 | import org.junit.jupiter.api.Test; 8 | 9 | 10 | public class Java13Test { 11 | 12 | @Test 13 | public void testWithTextBlock() { 14 | String textBlock = """ 15 | line1 16 | line2"""; 17 | 18 | assertEquals(wrapped(textBlock), TestHelper.getLastRecordedStateForVariable("textBlock")); 19 | assertEquals("line1\nline2", textBlock); 20 | } 21 | 22 | @Test 23 | public void testWithSwitchExpression() { 24 | Day day = Day.WEDNESDAY; 25 | int j = switch (day) { 26 | case MONDAY -> 0; 27 | case TUESDAY -> 1; 28 | default -> { 29 | int k = day.toString().length(); 30 | assertEquals(Integer.toString(k), TestHelper.getLastRecordedStateForVariable("k")); 31 | assertEquals(9, k); 32 | yield k; 33 | } 34 | }; 35 | 36 | assertEquals(Integer.toString(j), TestHelper.getLastRecordedStateForVariable("j")); 37 | assertEquals(9, j); 38 | } 39 | 40 | private enum Day { 41 | MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /scott-examples/junit4/src/test/java/hu/advancedweb/example/ListTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import static org.junit.Assert.assertArrayEquals; 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import org.junit.Test; 14 | 15 | public class ListTest { 16 | 17 | @Test 18 | public void myTest() { 19 | Integer[] myArray = new Integer[] { 1, 4, 2, 4 }; 20 | List myList = Arrays.asList(myArray); 21 | 22 | Set mySet = new HashSet<>(myList); 23 | mySet.remove(4); 24 | 25 | assertTrue(mySet.contains(4)); 26 | } 27 | 28 | @Test 29 | public void test_1() { 30 | Integer[] myArray = new Integer[] { 1, 4, 2, 4 }; 31 | List myList = Arrays.asList(myArray); 32 | 33 | Set mySet = new HashSet(myList); 34 | mySet.remove(1); 35 | 36 | assertEquals(myList.size(), mySet.size()); 37 | } 38 | 39 | @Test 40 | public void test_2() throws Throwable { 41 | Integer[] array = new Integer[] { 1, 4, 2, 3 }; 42 | List list = Arrays.asList(array); 43 | Collections.sort(list); 44 | 45 | assertArrayEquals(array, new Integer[] { 1, 4, 2, 3 }); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/MultiInstrumentationTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotEquals; 5 | 6 | import org.junit.Test; 7 | 8 | import hu.advancedweb.scott.helper.InstrumentedObject; 9 | import hu.advancedweb.scott.helper.TestScottRuntime; 10 | import hu.advancedweb.scott.instrumentation.transformation.ScottClassTransformer; 11 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 12 | 13 | public class MultiInstrumentationTest { 14 | 15 | @Test 16 | public void recordReturnFromLambda() throws Exception { 17 | byte[] originalClass = InstrumentedObject.getBytes("hu.advancedweb.scott.examples.ClassWithTrys"); 18 | byte[] instrumentedClass = new ScottClassTransformer().transform(originalClass, config()); 19 | byte[] doubleInstrumentedClass = new ScottClassTransformer().transform(instrumentedClass, config()); 20 | 21 | assertNotEquals(originalClass.length, instrumentedClass.length); 22 | assertEquals(instrumentedClass.length, doubleInstrumentedClass.length); 23 | } 24 | 25 | private Configuration config() { 26 | Configuration config = new Configuration.Builder() 27 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 28 | .build(); 29 | 30 | return config; 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - Cucumber Example project 2 | ============================================== 3 | 4 | This project contains the necessary configuration 5 | to use Scott with Cucumber (```io.cucumber:cucumber-java```). (See [pom.xml](https://github.com/dodie/scott/blob/master/scott-examples/cucumber/pom.xml) 6 | for the project setup and the [FeatureTest](https://github.com/dodie/scott/blob/master/scott-examples/cucumber-cucumber-io/src/test/java/hu/advancedweb/example/FeatureTest.java) 7 | for the ```@CucumberOptions```.) and a bunch of failing tests to demonstrate the detailed failure messages. 8 | 9 | Scott for Cucumber **tracks whole scenarios**, and in case of a failure it prints the details of every step involved. 10 | 11 | This feature provides valuable information if a test fails in a CI environment, as it can make it much easier to reproduce and fix browser-based tests, especially for flaky tests. 12 | 13 | 14 | Usage 15 | ----- 16 | Run ``` mvn install ``` to see the tests failing. For a Docker-based setup, see the [development guide](https://github.com/dodie/scott/blob/master/docs/development-guide.md). 17 | 18 | 19 | Demo 20 | ---- 21 | Scott tracks the input parameters and field values, and present them in the test reports. 22 | 23 | **HTML report:** 24 | ![HTML](https://github.com/dodie/scott-showcase/blob/master/cucumber_html2.png "HTML") 25 | 26 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/TestHelper.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import hu.advancedweb.scott.runtime.track.StateData; 8 | import hu.advancedweb.scott.runtime.track.StateRegistry; 9 | 10 | class TestHelper { 11 | 12 | static String getLastRecordedStateForVariable(String variableName) { 13 | List states = new ArrayList(StateRegistry.getLocalVariableStates()); 14 | Collections.reverse(states); 15 | 16 | for (StateData localVariableState : states) { 17 | if (localVariableState.name.equals(variableName)) { 18 | return localVariableState.value; 19 | } 20 | } 21 | 22 | return null; 23 | } 24 | 25 | public static String getLastRecordedStateForField(String fieldName) { 26 | List states = new ArrayList(StateRegistry.getFieldStates()); 27 | Collections.reverse(states); 28 | 29 | for (StateData localVariableState : states) { 30 | if (localVariableState.name.equals(fieldName)) { 31 | return localVariableState.value; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | 38 | public static String wrapped(String original) { 39 | char wrappingChar = '"'; 40 | return new StringBuilder().append(wrappingChar).append(original).append(wrappingChar).toString(); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/helper/TestHelper.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.helper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import hu.advancedweb.scott.runtime.track.StateData; 8 | import hu.advancedweb.scott.runtime.track.StateRegistry; 9 | 10 | public class TestHelper { 11 | 12 | public static String getLastRecordedStateForVariable(String variableName) { 13 | List states = new ArrayList(StateRegistry.getLocalVariableStates()); 14 | Collections.reverse(states); 15 | 16 | for (StateData localVariableState : states) { 17 | if (localVariableState.name.equals(variableName)) { 18 | return localVariableState.value; 19 | } 20 | } 21 | 22 | return null; 23 | } 24 | 25 | public static String getLastRecordedStateForField(String fieldName) { 26 | List states = new ArrayList(StateRegistry.getFieldStates()); 27 | Collections.reverse(states); 28 | 29 | for (StateData localVariableState : states) { 30 | if (localVariableState.name.equals(fieldName)) { 31 | return localVariableState.value; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | 38 | public static String wrapped(String original) { 39 | char wrappingChar = '"'; 40 | return new StringBuilder().append(wrappingChar).append(original).append(wrappingChar).toString(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /scott-tests/configuration-example-classes/src/main/java/hu/advancedweb/scott/examples/ClassWithLambdas.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.examples; 2 | 3 | 4 | 5 | import java.util.Arrays; 6 | import java.util.function.Consumer; 7 | import java.util.function.Supplier; 8 | import java.util.stream.DoubleStream; 9 | import java.util.stream.IntStream; 10 | import java.util.stream.LongStream; 11 | 12 | public class ClassWithLambdas { 13 | 14 | public void primitiveBooleanTrue() { 15 | Arrays.asList("abc").stream().anyMatch(s -> s.contains("abc")); 16 | } 17 | 18 | public void primitiveBooleanFalse() { 19 | Arrays.asList("abc").stream().anyMatch(s -> s.contains("not in the list")); 20 | } 21 | 22 | public void primitiveInt() { 23 | IntStream.builder().add(Integer.MAX_VALUE).build().reduce(0, (a, b) -> a + b); 24 | } 25 | 26 | public void primitiveDouble() { 27 | DoubleStream.builder().add(Double.MAX_VALUE).build().reduce(0D, (a, b) -> a + b); 28 | } 29 | 30 | public void primitiveLong() { 31 | LongStream.builder().add(Long.MAX_VALUE).build().reduce(0L, (a, b) -> a + b); 32 | } 33 | 34 | public void object() { 35 | Supplier objectSupplier = () -> "Hello world"; 36 | objectSupplier.get(); 37 | } 38 | 39 | public void simpleReturn() { 40 | Consumer consumer = (s) -> { 41 | if (s == null) { 42 | return; 43 | } 44 | s.length(); 45 | }; 46 | consumer.accept(null); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Maven 2 | # Build your Java project and run tests with Apache Maven. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/java 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | - task: SonarCloudPrepare@1 14 | inputs: 15 | SonarCloud: 'SonarCloud' 16 | organization: 'dodie' 17 | scannerMode: 'Other' 18 | - task: Maven@3 19 | displayName: 'Build and scan core package on Java 11' 20 | inputs: 21 | mavenPomFile: 'scott/pom.xml' 22 | mavenOptions: '-Xmx3072m' 23 | javaHomeOption: 'JDKVersion' 24 | jdkVersionOption: '1.11' 25 | jdkArchitectureOption: 'x64' 26 | publishJUnitResults: false 27 | testResultsFiles: '**/surefire-reports/TEST-*.xml' 28 | goals: 'package' 29 | sonarQubeRunAnalysis: true 30 | - task: Docker@2 31 | displayName: 'Build and test on Java 17' 32 | inputs: 33 | command: run 34 | arguments: -t --rm -v "$(Build.SourcesDirectory)":/usr/src/mymaven -v "$(Agent.HomeDirectory)/.m2":/root/.m2 -v "$(Build.SourcesDirectory)/target:/usr/src/mymaven/target" -w /usr/src/mymaven maven:3.6.3-openjdk-17 mvn install 35 | - script: | 36 | cd scott-gradle-plugin 37 | chmod +x ./gradlew 38 | ./gradlew build 39 | displayName: 'Gradle Plugin' 40 | - task: SonarCloudPublish@1 41 | inputs: 42 | pollingTimeoutSec: '300' 43 | 44 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/helper/ClassFileStructurePrinter.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.helper; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import java.util.List; 6 | 7 | import org.objectweb.asm.ClassReader; 8 | import org.objectweb.asm.tree.ClassNode; 9 | import org.objectweb.asm.tree.InsnList; 10 | import org.objectweb.asm.tree.MethodNode; 11 | import org.objectweb.asm.util.Printer; 12 | import org.objectweb.asm.util.Textifier; 13 | import org.objectweb.asm.util.TraceMethodVisitor; 14 | 15 | public class ClassFileStructurePrinter { 16 | 17 | public static void viewByteCode(byte[] bytecode) { 18 | ClassReader classReader = new ClassReader(bytecode); 19 | ClassNode classNode = new ClassNode(); 20 | classReader.accept(classNode, 0); 21 | final List methodNodes = classNode.methods; 22 | Printer printer = new Textifier(); 23 | TraceMethodVisitor traceMethodVisitor = new TraceMethodVisitor(printer); 24 | 25 | for (MethodNode methodNode : methodNodes) { 26 | InsnList insnList = methodNode.instructions; 27 | System.out.println(methodNode.name); 28 | for (int i = 0; i < insnList.size(); i++) { 29 | insnList.get(i).accept(traceMethodVisitor); 30 | StringWriter sw = new StringWriter(); 31 | printer.print(new PrintWriter(sw)); 32 | printer.getText().clear(); 33 | System.out.print(sw.toString()); 34 | } 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/UnhandledExceptionRecordingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.anyInt; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import org.junit.Test; 10 | 11 | import hu.advancedweb.scott.helper.InstrumentedObject; 12 | import hu.advancedweb.scott.helper.TestScottRuntime; 13 | import hu.advancedweb.scott.helper.TestScottRuntimeVerifier; 14 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 15 | 16 | public class UnhandledExceptionRecordingTest { 17 | 18 | @Test 19 | public void recordUnhandledExceptions() throws Exception { 20 | Configuration config = new Configuration.Builder() 21 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 22 | .build(); 23 | 24 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 25 | try { 26 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithUnhandledException", config).invokeMethod("boom"); 27 | } catch (Exception e) { 28 | // Ignore 29 | } 30 | 31 | // Tracking happens 2 times: one for the lambda in the method, and one for the method itself. 32 | verify(testRuntime, times(2)).trackUnhandledException(any(), anyInt(), any(), any()); 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | configuration-based-tests 6 | 4.0.1 7 | 8 | UTF-8 9 | 10 | Scott Test Reporter - Configuration-based tests 11 | 12 | 13 | 14 | 15 | org.apache.maven.plugins 16 | maven-compiler-plugin 17 | 3.8.1 18 | 19 | 1.8 20 | 1.8 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | junit 29 | junit 30 | 4.13.2 31 | test 32 | 33 | 34 | org.mockito 35 | mockito-core 36 | 3.12.4 37 | test 38 | 39 | 40 | hu.advancedweb 41 | scott 42 | ${project.version} 43 | test 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/ScottReportingRule.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | 7 | import hu.advancedweb.scott.runtime.report.FailureRenderer; 8 | 9 | /** 10 | * To produce the detailed failure reports use this JUnit Rule. 11 | * 12 | * @author David Csakvari 13 | */ 14 | public class ScottReportingRule implements TestRule { 15 | 16 | @Override 17 | public Statement apply(final Statement base, final Description description) { 18 | return new Statement() { 19 | @Override 20 | public void evaluate() throws Throwable { 21 | String testClassName = description.getTestClass() != null ? description.getTestClass().getTypeName() : null; 22 | String testMethodName = description.getMethodName(); 23 | 24 | /* 25 | * Evaluate test and in case of a failure 26 | * throw a new error with the correct exception type 27 | * that contains Scott's output and the original cause. 28 | */ 29 | try { 30 | base.evaluate(); 31 | } catch (AssertionError assertionError) { 32 | throw new AssertionError(FailureRenderer.render(testClassName, testMethodName, assertionError), assertionError); 33 | } catch (Error error) { 34 | throw new Error(FailureRenderer.render(testClassName, testMethodName, error), error); 35 | } catch (Throwable throwable) { 36 | throw new Throwable(FailureRenderer.render(testClassName, testMethodName, throwable), throwable); 37 | } 38 | } 39 | }; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /scott-gradle-plugin/src/main/java/hu/advancedweb/scott/ScottPluginExtension.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import java.util.List; 4 | 5 | public class ScottPluginExtension { 6 | 7 | private String toolVersion; 8 | 9 | private List trackMethodAnnotations; 10 | 11 | private List junit4RuleMethodAnnotations; 12 | 13 | private List junit5RuleMethodAnnotations; 14 | 15 | public ScottPluginExtension(String toolVersion){ 16 | this.toolVersion = toolVersion; 17 | } 18 | 19 | public String getToolVersion() { 20 | return toolVersion; 21 | } 22 | 23 | public void setToolVersion(String toolVersion) { 24 | this.toolVersion = toolVersion; 25 | } 26 | 27 | public List getTrackMethodAnnotations() { 28 | return trackMethodAnnotations; 29 | } 30 | 31 | public void setTrackMethodAnnotations(List trackMethodAnnotations) { 32 | this.trackMethodAnnotations = trackMethodAnnotations; 33 | } 34 | 35 | public List getJunit4RuleMethodAnnotations() { 36 | return junit4RuleMethodAnnotations; 37 | } 38 | 39 | public void setJunit4RuleMethodAnnotations(List junit4RuleMethodAnnotations) { 40 | this.junit4RuleMethodAnnotations = junit4RuleMethodAnnotations; 41 | } 42 | 43 | public List getJunit5RuleMethodAnnotations() { 44 | return junit5RuleMethodAnnotations; 45 | } 46 | 47 | public void setJunit5RuleMethodAnnotations(List junit5RuleMethodAnnotations) { 48 | this.junit5RuleMethodAnnotations = junit5RuleMethodAnnotations; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/DisabledFeaturesConfigTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.mockito.Mockito.mock; 4 | import static org.mockito.Mockito.verifyZeroInteractions; 5 | 6 | import org.junit.Test; 7 | 8 | import hu.advancedweb.scott.helper.InstrumentedObject; 9 | import hu.advancedweb.scott.helper.TestScottRuntime; 10 | import hu.advancedweb.scott.helper.TestScottRuntimeVerifier; 11 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 12 | 13 | public class DisabledFeaturesConfigTest { 14 | 15 | @Test 16 | public void withDisabledConfigNoInstrumentationHappens() throws Exception { 17 | Configuration disabledConfig = new Configuration.Builder() 18 | .setTrackLocalVariableAssignments(false) 19 | .setIncludeLambdas(false) 20 | .setTrackLocalVariableIncrements(false) 21 | .setTrackLocalVariablesAfterEveryMethodCall(false) 22 | .setTrackFieldAssignments(false) 23 | .setTrackFieldsAfterEveryMethodCall(false) 24 | .setTrackMethodStart(false) 25 | .setTrackReturn(false) 26 | .setTrackUnhandledException(false) 27 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 28 | .build(); 29 | 30 | assertNoTrackingMethodInvoked( 31 | "hu.advancedweb.scott.examples.ClassWithFeatures", 32 | disabledConfig); 33 | } 34 | 35 | private void assertNoTrackingMethodInvoked(String name, Configuration configuration) { 36 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 37 | InstrumentedObject.create(name, configuration).invokeMethod("ii"); 38 | verifyZeroInteractions(testRuntime); 39 | }); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /scott-examples/junit4/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | scott-example-junit4 6 | 0.0.1-SNAPSHOT 7 | 8 | 9 | UTF-8 10 | 11 | 12 | 13 | 14 | 15 | hu.advancedweb 16 | scott-maven-plugin 17 | 4.0.1 18 | 19 | 20 | 21 | prepare-agent 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-surefire-plugin 29 | 2.22.2 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-compiler-plugin 34 | 3.8.1 35 | 36 | 1.8 37 | 1.8 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | hu.advancedweb 46 | scott 47 | 4.0.1 48 | test 49 | 50 | 51 | junit 52 | junit 53 | 4.13.2 54 | test 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/runtime/report/ScottReportTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.report; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.UUID; 8 | 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.core.Is.is; 11 | 12 | public class ScottReportTest { 13 | 14 | @Test 15 | public void verifyVariableMapSnapshot() { 16 | ScottReport report = new ScottReport(); 17 | report.addSnapshot(42, "foo", UUID.randomUUID().toString()); 18 | report.addSnapshot(42, "foo", UUID.randomUUID().toString()); 19 | report.addSnapshot(42, "foo", UUID.randomUUID().toString()); 20 | 21 | report.addSnapshot(42, "bar", UUID.randomUUID().toString()); 22 | report.addSnapshot(42, "bar", UUID.randomUUID().toString()); 23 | 24 | report.addSnapshot(666, "beast", UUID.randomUUID().toString()); 25 | 26 | Map> variableMapSnapshot = report.getVariableMapSnapshot(42); 27 | 28 | assertThat("line 42 contains foo",variableMapSnapshot.containsKey("foo"), is(Boolean.TRUE)); 29 | assertThat("line 42 foo with 3 values",variableMapSnapshot.get("foo").size(), is(3)); 30 | 31 | assertThat("line 42 contains bar",variableMapSnapshot.containsKey("bar"), is(Boolean.TRUE)); 32 | assertThat("line 42 bar with 2 values",variableMapSnapshot.get("bar").size(), is(2)); 33 | 34 | variableMapSnapshot = report.getVariableMapSnapshot(666); 35 | assertThat("line 666 contains beast",variableMapSnapshot.containsKey("beast"), is(Boolean.TRUE)); 36 | assertThat("line 666 beast with 1 value",variableMapSnapshot.get("beast").size(), is(1)); 37 | 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/report/javasource/SourcePathResolver.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.report.javasource; 2 | 3 | 4 | import java.io.IOException; 5 | import java.nio.file.FileSystems; 6 | import java.nio.file.FileVisitResult; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.PathMatcher; 10 | import java.nio.file.Paths; 11 | import java.nio.file.SimpleFileVisitor; 12 | import java.nio.file.attribute.BasicFileAttributes; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * Performs Java source lookup. 18 | * 19 | * @author David Csakvari 20 | */ 21 | public class SourcePathResolver { 22 | 23 | public String getSourcePath(final String fqn) throws IOException { 24 | final String currentDir = System.getProperty("user.dir").replace("\\", "/"); 25 | final String relativeFilePath = fqn.replace(".", "/") + ".java"; 26 | 27 | final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + currentDir + "/**/" + relativeFilePath); 28 | final List result = new ArrayList<>(); 29 | Files.walkFileTree(Paths.get(currentDir), new SimpleFileVisitor() { 30 | @Override 31 | public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { 32 | if (matcher.matches(path)) { 33 | result.add(path.toString()); 34 | return FileVisitResult.TERMINATE; 35 | } else { 36 | return FileVisitResult.CONTINUE; 37 | } 38 | } 39 | 40 | @Override 41 | public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 42 | return FileVisitResult.CONTINUE; 43 | } 44 | }); 45 | 46 | if (result.isEmpty()) { 47 | return null; 48 | } else { 49 | return result.get(0); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /docs/release_guide.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | - Import the keys: 4 | - `gpg2 --list-keys` 5 | - Observe that there are no keys imported. 6 | - `gpg2 --import private-keys` and `gpg2 --import pub-keys` 7 | - `gpg2 --list-keys` 8 | - Observe that the keys are imported. 9 | - Create a new `servers/server` entry in the `.m2/settings.xml` file with `nexus-releases` as an id. The specify the `username` and `password` for the `oss.sonatype.org` Nexus user. 10 | 11 | 12 | # Release 13 | 14 | - Bump version. 15 | - Build everything: 16 | - Run `mvn clean install` in the root of the project. 17 | - Run`./gradlew build` on the `scott-gradle-plugin`. 18 | - Deploy Maven Artifacts: 19 | - For `scott/scott` and `scott/scott-maven-plugin` do the following: (with the appropriate version numbers): 20 | - `mvn gpg:sign-and-deploy-file -Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ -DrepositoryId=nexus-releases -DpomFile=pom.xml -Dfile=target/scott-1.0.0.jar` 21 | - `mvn gpg:sign-and-deploy-file -Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ -DrepositoryId=nexus-releases -DpomFile=pom.xml -Dfile=target/scott-1.0.0-sources.jar -Dclassifier=sources` 22 | - `mvn gpg:sign-and-deploy-file -Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ -DrepositoryId=nexus-releases -DpomFile=pom.xml -Dfile=target/scott-1.0.0-javadoc.jar -Dclassifier=javadoc` 23 | - Sign in to [https://oss.sonatype.org](https://oss.sonatype.org). Find the new repo under "Staging repositories". Make sure that the contents are OK, then hit Close, then Release. 24 | - Deploy `scott/scott-gradle-plugin`: 25 | - `./gradlew publishPlugins` 26 | 27 | 28 | The Gradle plugin will be released after manual approval. The Maven artifacts will be available in Maven Central after a few hours. 29 | 30 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/ScottAgent.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation; 2 | 3 | import java.lang.instrument.ClassFileTransformer; 4 | import java.lang.instrument.IllegalClassFormatException; 5 | import java.lang.instrument.Instrumentation; 6 | import java.security.ProtectionDomain; 7 | 8 | import hu.advancedweb.scott.instrumentation.transformation.ScottClassTransformer; 9 | 10 | /** 11 | * Scott's Java Agent that instruments classes at class-load time. 12 | * 13 | * For more information, see: 14 | * https://docs.oracle.com/en/java/javase/11/docs/api/java.instrument/java/lang/instrument/package-summary.html 15 | * 16 | * @author David Csakvari 17 | */ 18 | public class ScottAgent { 19 | 20 | private ScottAgent() { 21 | // Intended to be used as an agent and not to be instantiated directly. 22 | } 23 | 24 | public static void premain(String agentArgument, Instrumentation instrumentation) { 25 | instrumentation.addTransformer(new ClassFileTransformer() { 26 | @Override 27 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 28 | if (loader == null) { 29 | /* 30 | * Leave the class alone, if it is being loaded by the Bootstrap classloader, 31 | * as we don't want to do anything with JDK libs. See Issue #22. 32 | */ 33 | return classfileBuffer; 34 | } else { 35 | try { 36 | return new ScottClassTransformer().transform(classfileBuffer, ScottConfigurer.getConfiguration()); 37 | } catch (Exception e) { 38 | System.err.println("Scott: test instrumentation failed for " + className + "!"); 39 | e.printStackTrace(); 40 | throw e; 41 | } 42 | } 43 | } 44 | }); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/FieldRecordingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.TestHelper.wrapped; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import org.junit.Test; 8 | 9 | public class FieldRecordingTest { 10 | 11 | byte b = 0; 12 | short s = 0; 13 | int i = 0; 14 | long l = 0L; 15 | float f = 0.0F; 16 | double d = 0.0D; 17 | boolean bool = false; 18 | char c = 'i'; 19 | String object = "initial"; 20 | 21 | 22 | @Test 23 | public void recordInteger() throws Exception { 24 | i = 5; 25 | assertThat(TestHelper.getLastRecordedStateForField("this.i"), equalTo(Integer.toString(i))); 26 | } 27 | 28 | @Test 29 | public void recordShort() throws Exception { 30 | s = 500; 31 | assertThat(TestHelper.getLastRecordedStateForField("this.s"), equalTo(Short.toString(s))); 32 | } 33 | 34 | @Test 35 | public void recordLong() throws Exception { 36 | l = 1000L; 37 | assertThat(TestHelper.getLastRecordedStateForField("this.l"), equalTo(Long.toString(l))); 38 | } 39 | 40 | @Test 41 | public void recordDouble() throws Exception { 42 | d = 5.5D; 43 | assertThat(TestHelper.getLastRecordedStateForField("this.d"), equalTo(Double.toString(d))); 44 | } 45 | 46 | @Test 47 | public void recordFloat() throws Exception { 48 | f = 5.5F; 49 | assertThat(TestHelper.getLastRecordedStateForField("this.f"), equalTo(Float.toString(f))); 50 | } 51 | 52 | @Test 53 | public void recordBoolean() throws Exception { 54 | bool = true; 55 | assertThat(TestHelper.getLastRecordedStateForField("this.bool"), equalTo(Boolean.toString(bool))); 56 | } 57 | 58 | @Test 59 | public void recordString() throws Exception { 60 | object = "Hello World!"; 61 | assertThat(TestHelper.getLastRecordedStateForField("this.object"), equalTo(wrapped(object))); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/TryTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.anyInt; 5 | import static org.mockito.ArgumentMatchers.eq; 6 | import static org.mockito.Mockito.mock; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import org.junit.Test; 10 | 11 | import hu.advancedweb.scott.helper.InstrumentedObject; 12 | import hu.advancedweb.scott.helper.TestScottRuntime; 13 | import hu.advancedweb.scott.helper.TestScottRuntimeVerifier; 14 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 15 | 16 | public class TryTest { 17 | 18 | @Test 19 | public void recordReturnFromLambda() throws Exception { 20 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 21 | InstrumentedObject instrumentedObject = instrumentExampleClass(); 22 | 23 | Object obj = instrumentedObject.invokeMethod("hello"); 24 | Object obj2 = instrumentedObject.invokeMethod("hello2"); 25 | 26 | // This test is to verify that the instrumentation of these classes produce valid bytecode. 27 | verify(testRuntime).trackMethodStart(anyInt(), eq("hello"), any()); 28 | verify(testRuntime).trackMethodStart(anyInt(), eq("hello2"), any()); 29 | }); 30 | } 31 | 32 | private InstrumentedObject instrumentExampleClass() { 33 | Configuration config = new Configuration.Builder() 34 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 35 | .setIncludeLambdasOnlyWhenOtherInstrumentationIsInPlace(true) 36 | .setTrackLocalVariablesAfterEveryMethodCall(false) 37 | .setTrackFieldAssignments(false) 38 | .setTrackFieldsAfterEveryMethodCall(false) 39 | .setTrackReturn(false) 40 | .setTrackUnhandledException(false) 41 | .build(); 42 | 43 | return InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithTrys", config); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/RecordIncMutationTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.hamcrest.CoreMatchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import org.junit.Test; 7 | 8 | public class RecordIncMutationTest { 9 | 10 | @Test 11 | public void primitiveInc() throws Exception { 12 | int i = 0; 13 | i++; 14 | assertThat(TestHelper.getLastRecordedStateForVariable("i"), equalTo(Integer.toString(i))); 15 | 16 | long l = 0L; 17 | l++; 18 | assertThat(TestHelper.getLastRecordedStateForVariable("l"), equalTo(Long.toString(l))); 19 | 20 | float f = 0F; 21 | f++; 22 | assertThat(TestHelper.getLastRecordedStateForVariable("f"), equalTo(Float.toString(f))); 23 | 24 | double d = 0D; 25 | d++; 26 | assertThat(TestHelper.getLastRecordedStateForVariable("d"), equalTo(Double.toString(d))); 27 | } 28 | 29 | @Test 30 | public void boxedInc() throws Exception { 31 | Integer i = Integer.valueOf("0"); 32 | i++; 33 | assertThat(TestHelper.getLastRecordedStateForVariable("i"), equalTo(Integer.toString(i))); 34 | 35 | Long l = Long.valueOf("0"); 36 | l++; 37 | assertThat(TestHelper.getLastRecordedStateForVariable("l"), equalTo(Long.toString(l))); 38 | 39 | Float f = Float.valueOf("0"); 40 | f++; 41 | assertThat(TestHelper.getLastRecordedStateForVariable("f"), equalTo(Float.toString(f))); 42 | 43 | Double d = Double.valueOf("0"); 44 | d++; 45 | assertThat(TestHelper.getLastRecordedStateForVariable("d"), equalTo(Double.toString(d))); 46 | } 47 | 48 | int a = 0; 49 | Integer aA = Integer.valueOf("0"); 50 | 51 | @Test 52 | public void fieldInc() throws Exception { 53 | this.a++; 54 | this.aA++; 55 | 56 | assertThat(TestHelper.getLastRecordedStateForField("this.a"), equalTo(Integer.toString(this.a))); 57 | assertThat(TestHelper.getLastRecordedStateForField("this.aA"), equalTo(Integer.toString(this.aA))); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/AccessedField.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | /** 4 | * Metadata of a field accessed by a test case. 5 | * 6 | * @author David Csakvari 7 | */ 8 | class AccessedField { 9 | 10 | final String owner; 11 | final String name; 12 | final String desc; 13 | final boolean isStatic; 14 | 15 | public AccessedField(String owner, String name, String desc, boolean isStatic) { 16 | this.owner = owner; 17 | this.name = name; 18 | this.desc = desc; 19 | this.isStatic = isStatic; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "AccessedField [owner=" + owner + ", name=" + name + ", desc=" + desc + ", isStatic=" + isStatic + "]"; 25 | } 26 | 27 | @Override 28 | public int hashCode() { 29 | final int prime = 31; 30 | int result = 1; 31 | result = prime * result + ((desc == null) ? 0 : desc.hashCode()); 32 | result = prime * result + (isStatic ? 1231 : 1237); 33 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 34 | result = prime * result + ((owner == null) ? 0 : owner.hashCode()); 35 | return result; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object obj) { 40 | if (this == obj) 41 | return true; 42 | if (obj == null) 43 | return false; 44 | if (getClass() != obj.getClass()) 45 | return false; 46 | AccessedField other = (AccessedField) obj; 47 | if (desc == null) { 48 | if (other.desc != null) 49 | return false; 50 | } else if (!desc.equals(other.desc)) 51 | return false; 52 | if (isStatic != other.isStatic) 53 | return false; 54 | if (name == null) { 55 | if (other.name != null) 56 | return false; 57 | } else if (!name.equals(other.name)) 58 | return false; 59 | if (owner == null) { 60 | if (other.owner != null) 61 | return false; 62 | } else if (!owner.equals(other.owner)) 63 | return false; 64 | return true; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /docs/architecture.xml: -------------------------------------------------------------------------------- 1 | 5VrbcqM4EP0aPyaFEGD8mPiyO1szVVvJ1O7sowIyZoKRR4jY3q/fBiRugsSxIcnMOg+xWhfk092nW40meL49/MbJbvOF+TSamIZ/mODFxITP1IF/meRYSKYWKgQBD/1CVBPch/9SKTSkNA19mjQGCsYiEe6aQo/FMfVEQ0Y4Z/vmsDWLmk/dkYBqgnuPRLr079AXm0Lq2kYl/52GwUY9GRmy54F4jwFnaSyfNzHxOv8U3Vui1pLjkw3x2b4mwssJnnPGRPFte5jTKMNWwVbMW/X0lvvmNBanTLDMYsYTiVKqtpxvTBwVGDT2bzJMoRWzGIS3Pkk2NFsBQWMjtpH8mgjOHkvEMEjWLBZSvWbWLhanvgZ/tWFUwgDmRdmWCn6EIftKD0oNm5oKlIzTiIjwqbk8keYQlMuVT/iThfBg05CG6kyN5pSEpdyjclQdyNZEqzVPEB5Qoc2DL7VfVYlytXSrCLuaihKPCXHN01iEW6rpKze/XD0GIL7fhILe74iX9e7BXTu0sg6jaM4ixvMF8HK2XKxskJMoDGKQRXQNMN8+US5CcJIbKRasXEw+XeKdDaSH12pYTnAlkiWwuGjXDMDpsgDkmP3abuD/DNim1QN2GINxp1v4DWBeLB4BdDf7ey/Qr2zrZdgtZzTYsQb7H399GR7k1WppzVcfBmQ0M04F2R4AZA3je2Apke40nOEnii5mV0jKMFAHV4oUrh5AQnkHstvQ97PHvKQ97JTR4jLMMW5hjmwd8w7ITeNyxHUyuevh7F8IcdwicGS+HeAIaYjr6QwkH8qrGRcbFrCYRMtKeuul/KkrvflOhThKtEgqGIiqBT6zjDNeToNqML+cBsHG8/xDRsZCVKQWk1rCrOvi5DToVGT1JOQzI/7EhEQfo4yN77MweRNQme8MTNvz5Ww1ECHYqGmfJtbtE3UlGLMhDNT+OQwUwOTHb1JjeeOfrHFtn228CvWG9VoXWm8+FQ4n5FgbsMuS7qS2skrVlQUg12pYAJ62DkntCXaL0l49wZq5LSspNn3umUCB2Y7lNX/0IpIACEYEbhrGwQd3StN5R6fsyIw+mlO+6vDccjxLd7w+dQweNpCeAi1COFGBtWRboknhqMNa5sJeugtrpHBh2frBaDzL1POZT+VB9CcBsO3abwug0+HaTiQkBg3cnB8pUx1XSY7ODQxA092h6oRvQfaf0yBMsoRbrgYbKRYsugfkjyoU59G3CsYfgVu6UtLZ8/ZgXCPDdZsmIVni3OKdGsLW64ReWm9Deqr7Fdwsj6HGipMt3TP+OIDTJdCdrQrNjujqzpfz+TAuaNut4Kqy+bfwQDxkLK35wrThDMMGWnoIRc3joKWecaabzDpCsH1hCD4r93XUYVzVu2WZry+TtSz83PiLE1msF/6G4ec1yxIMCn6TwbMmYVQ4cBYxT+VsvSjDKTyXPOQDMjeXcMNo+3ZiL15VjonIA41uy/dF7UpPJz930IB8BSY3NSnLKHWTLHywlx+AkW0VKM+lYPXqb9YwF4SbCwxD0LPTCLo8Cn0CVQR8pLr9wlqilT0OUb9pqqROXKMmLWPxtDkMUSt2rBM1vrTEdhZRY6eZIjnG8yUH5LaJ3Wgp/0KiHiuR/pEWFO2xKAIeA2MyDZ8I8v8j6Z6EoCRpY2Y261ZoEMpG0+ai1giUbeplzzsKvMtrLM3pjvERisdDEnT7ZdLbEnRn0U9QEN3lp1G53IdFD71nKQXrWYOkHMU0fvhUnu7Va7qSg2qdHeNbhNUY3I6p1W2avEQbepNX3qFRUQ/VQl4tzPZU7XuYrLqgVB5Iv+YR2NBOqGeGVHV/64SQKu3iSt1KGvnOj6pItN7K27hlbi/eAVL23TJvt7VOz52gYeoY0KyujRXDq7t5ePkf -------------------------------------------------------------------------------- /scott-examples/junit4/src/test/java/hu/advancedweb/example/LambdaTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.example; 2 | 3 | import static org.hamcrest.CoreMatchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import java.util.function.Function; 7 | 8 | import org.junit.Test; 9 | 10 | public class LambdaTest { 11 | 12 | @Test 13 | public void test_with_lambda() throws Exception { 14 | Function generatePalindrome = input -> { 15 | StringBuilder sb = new StringBuilder(); 16 | sb.append(input); 17 | sb.reverse(); 18 | 19 | String reversed = sb.toString(); 20 | 21 | String palindrome = input + reversed; 22 | return palindrome; 23 | }; 24 | 25 | String word = "cat"; 26 | 27 | String palindromized = generatePalindrome.apply(word); 28 | 29 | assertThat(palindromized, equalTo(word + word)); 30 | } 31 | 32 | @Test 33 | public void lambda_with_single_expression() throws Exception { 34 | Function lambda = a -> a + a; 35 | String result = lambda.apply("1"); 36 | assertThat(result, equalTo("2")); 37 | } 38 | 39 | @Test 40 | public void lambda_with_single_fun_expression() throws Exception { 41 | Function> lambda = a -> b -> a + b; 42 | String result = lambda.apply("1").apply("2"); 43 | assertThat(result, equalTo("3")); 44 | } 45 | 46 | @Test 47 | public void lambda_with_single_expression_2() throws Exception { 48 | Function lambda = a -> {return a + a;}; 49 | String result = lambda.apply("1"); 50 | assertThat(result, equalTo("2")); 51 | } 52 | 53 | @Test 54 | public void lambda_with_single_expression_3() throws Exception { 55 | Function lambda = a -> 56 | a + a; 57 | String result = lambda.apply("1"); 58 | assertThat(result, equalTo("2")); 59 | } 60 | 61 | 62 | @Test 63 | public void lambda_with_single_expression_4() throws Exception { 64 | Function lambda = a -> { 65 | return a; 66 | }; 67 | String result = lambda.apply("1"); 68 | assertThat(result, equalTo("2")); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/ScottClassFileTransformer.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.FileSystems; 5 | import java.nio.file.FileVisitResult; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.PathMatcher; 9 | import java.nio.file.Paths; 10 | import java.nio.file.SimpleFileVisitor; 11 | import java.nio.file.attribute.BasicFileAttributes; 12 | 13 | import hu.advancedweb.scott.instrumentation.transformation.ScottClassTransformer; 14 | 15 | /** 16 | * Transformer that enables directly transforming class files in the given folder. 17 | * It makes possible possible to integrate Scott as a post build step. 18 | * 19 | * When using it, make sure to have all runtime dependencies of the code available 20 | * on the classpath. For more information, see Issue #79 21 | * 22 | * @author David Csakvari 23 | */ 24 | public class ScottClassFileTransformer { 25 | 26 | public static void main(String[] args) throws IOException { 27 | if (args.length != 1) { 28 | throw new IllegalArgumentException("Exactly one argument is required."); 29 | } 30 | final String rootPath = args[0]; 31 | final PathMatcher classMatcher = FileSystems.getDefault().getPathMatcher("glob:**.class"); 32 | 33 | Files.walkFileTree(Paths.get(rootPath), new SimpleFileVisitor() { 34 | @Override 35 | public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { 36 | if (attrs.isRegularFile() && classMatcher.matches(path)) { 37 | transformClass(path); 38 | } 39 | return FileVisitResult.CONTINUE; 40 | } 41 | }); 42 | } 43 | 44 | private static void transformClass(Path path) { 45 | try { 46 | byte[] originalClass = Files.readAllBytes(path); 47 | byte[] instrumentedClass = new ScottClassTransformer().transform(originalClass, ScottConfigurer.getConfiguration()); 48 | Files.write(path, instrumentedClass); 49 | } catch (Exception e) { 50 | System.err.println("Could not instrument " + path); 51 | e.printStackTrace(); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/ConstructorTransformerMethodVisitor.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | 4 | import java.lang.instrument.UnmodifiableClassException; 5 | 6 | import hu.advancedweb.scott.runtime.ScottReportingRule; 7 | import org.objectweb.asm.MethodVisitor; 8 | import org.objectweb.asm.Opcodes; 9 | import org.objectweb.asm.Type; 10 | import org.objectweb.asm.tree.MethodNode; 11 | 12 | /** 13 | * Transformes the constructor to instantiate the `scottReportingRule` field. 14 | * 15 | * @author David Csakvari 16 | */ 17 | public class ConstructorTransformerMethodVisitor extends MethodNode { 18 | 19 | private String className; 20 | private MethodVisitor next; 21 | 22 | public ConstructorTransformerMethodVisitor(MethodVisitor next, final int access, final String name, final String desc, final String signature, final String[] exceptions, String className) { 23 | super(Opcodes.ASM9, access, name, desc, signature, exceptions); 24 | this.next = next; 25 | this.className = className; 26 | } 27 | 28 | @Override 29 | public void visitInsn(int opcode) { 30 | if ((opcode == Opcodes.ARETURN) || (opcode == Opcodes.IRETURN) 31 | || (opcode == Opcodes.LRETURN) 32 | || (opcode == Opcodes.FRETURN) 33 | || (opcode == Opcodes.DRETURN)) { 34 | throw new RuntimeException(new UnmodifiableClassException("Constructors are supposed to return void")); 35 | } 36 | if (opcode == Opcodes.RETURN) { 37 | super.visitVarInsn(Opcodes.ALOAD, 0); 38 | super.visitTypeInsn(Opcodes.NEW, Type.getInternalName(ScottReportingRule.class)); 39 | super.visitInsn(Opcodes.DUP); 40 | super.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ScottReportingRule.class), "", "()V", false); 41 | 42 | super.visitFieldInsn(Opcodes.PUTFIELD, 43 | className, "scottReportingRule", 44 | Type.getDescriptor(ScottReportingRule.class)); 45 | } 46 | 47 | super.visitInsn(opcode); 48 | } 49 | 50 | @Override 51 | public void visitEnd() { 52 | super.visitEnd(); 53 | accept(next); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/AssertionsTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static java.time.Duration.ofMillis; 4 | import static java.time.Duration.ofMinutes; 5 | import static org.junit.jupiter.api.Assertions.assertAll; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTimeout; 9 | import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Tests with assertions provided by JUnit 5 to check they don't crash Scott. 16 | */ 17 | public class AssertionsTest { 18 | 19 | @Test 20 | void standardAssertions() { 21 | assertEquals(2, 2); 22 | assertEquals(4, 4, "With message."); 23 | assertTrue(true, () -> "With message."); 24 | } 25 | 26 | @Test 27 | void groupedAssertions() { 28 | assertAll("User name", 29 | () -> assertEquals("John", "John"), 30 | () -> assertEquals("Doe", "Doe")); 31 | } 32 | 33 | @Test 34 | void exceptionTesting() { 35 | Throwable exception = assertThrows(IllegalArgumentException.class, () -> { 36 | throw new IllegalArgumentException("message"); 37 | }); 38 | assertEquals("message", exception.getMessage()); 39 | } 40 | 41 | @Test 42 | void timeoutNotExceeded() { 43 | assertTimeout(ofMinutes(2), () -> { 44 | assertEquals(2, 2); 45 | }); 46 | } 47 | 48 | @Test 49 | void timeoutExceededWithPreemptiveTermination() { 50 | assertTimeoutPreemptively(ofMillis(1000), () -> { 51 | assertEquals(2, 2); 52 | }); 53 | } 54 | 55 | @Test 56 | void timeoutNotExceededWithResult() { 57 | String actualResult = assertTimeout(ofMinutes(2), () -> { 58 | return "result"; 59 | }); 60 | assertEquals("result", actualResult); 61 | } 62 | 63 | @Test 64 | void timeoutNotExceededWithMethod() { 65 | String actualGreeting = assertTimeout(ofMinutes(2), AssertionsTest::greeting); 66 | assertEquals("hello world!", actualGreeting); 67 | } 68 | 69 | private static String greeting() { 70 | return "hello world!"; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/report/javasource/MethodBoundaryExtractor.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.report.javasource; 2 | 3 | import java.util.Optional; 4 | 5 | import com.github.javaparser.Range; 6 | import com.github.javaparser.ast.Node; 7 | import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; 8 | import com.github.javaparser.ast.body.MethodDeclaration; 9 | import com.github.javaparser.ast.visitor.VoidVisitorAdapter; 10 | 11 | import hu.advancedweb.scott.runtime.report.javasource.MethodBoundaryExtractor.Bounderies; 12 | 13 | /** 14 | * Extracts the begin and the end line for a method. 15 | * 16 | * @author David Csakvari 17 | */ 18 | class MethodBoundaryExtractor extends VoidVisitorAdapter { 19 | 20 | private final String methodName; 21 | private final String className; 22 | 23 | public MethodBoundaryExtractor(String className, String methodName) { 24 | this.methodName = methodName; 25 | this.className = className; 26 | } 27 | 28 | @Override 29 | public void visit(MethodDeclaration methodDeclaration, Bounderies boundaries) { 30 | if (methodDeclaration.getName().getIdentifier().equals(methodName) && isInTheCorrectClass(methodDeclaration)) { 31 | Optional range = methodDeclaration.getRange(); 32 | if (range.isPresent()) { 33 | boundaries.beginLine = range.get().begin.line; 34 | boundaries.endLine = range.get().end.line; 35 | } 36 | } 37 | } 38 | 39 | private boolean isInTheCorrectClass(MethodDeclaration methodDeclaration) { 40 | Node n = methodDeclaration; 41 | 42 | String containingClassName = ""; 43 | while (n != null) { 44 | if (n instanceof ClassOrInterfaceDeclaration) { 45 | ClassOrInterfaceDeclaration c = (ClassOrInterfaceDeclaration) n; 46 | containingClassName = c.getName() + "$" + containingClassName; 47 | } 48 | Optional no = n.getParentNode(); 49 | if (no.isEmpty()) { 50 | n = null; 51 | } else { 52 | n = no.get(); 53 | } 54 | } 55 | 56 | containingClassName = containingClassName.substring(0, containingClassName.length() - 1); 57 | return containingClassName.equals(className); 58 | } 59 | 60 | public static final class Bounderies { 61 | int beginLine; 62 | int endLine; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! Check the 4 | [development guide](https://github.com/dodie/scott/tree/master/docs/development-guide.md) for some important notes on how to build and debug Scott. 5 | If you are looking for issues that can get you started with the development, see [Issues marked with the help-wanted tag](https://github.com/dodie/scott/issues?q=is%3Aissue+label%3A%22help+wanted%22+is%3Aopen). 6 | 7 | ## Step 1. Fork and clone the repo 8 | 9 | ``` 10 | git clone git@github.com:your-username/scott.git 11 | ``` 12 | 13 | ## Step 2. Compile and run the tests to make sure everything works as expected 14 | 15 | ``` 16 | cd scott 17 | mvn clean install 18 | ``` 19 | 20 | ## Step 3. Check how Scott render failing tests 21 | Build the JUnit demo to get a sense what the end users of this library will see. 22 | 23 | ``` 24 | cd scott-examples/junit4 25 | mvn clean install 26 | ``` 27 | 28 | You should see a bunch of failing tests rendered something like the following example: 29 | 30 | ```java 31 | myTest(hu.advancedweb.example.ListTest) FAILED! 32 | 22| @Test 33 | 23| public void myTest() { 34 | 24| Integer[] myArray = new Integer[] { 1, 4, 2, 4 }; // myArray=[1, 4, 2, 4] 35 | 25| List myList = Arrays.asList(myArray); // myList=[1, 4, 2, 4] 36 | 26| 37 | 27| Set mySet = new HashSet<>(myList); // mySet=[1, 2, 4] 38 | 28| mySet.remove(4); // mySet=[1, 2] 39 | 29| 40 | 30|* assertTrue(mySet.contains(4)); // AssertionError 41 | 31| } 42 | ``` 43 | 44 | 45 | ## Step 4. Implement your changes 46 | Please use the following guidelines: 47 | 48 | - Make sure to respect existing formatting conventions. (Follow the same coding style as the code that you are modifying.) 49 | - Update documentation for the change you are making. 50 | - Add tests for your modifications where possible. 51 | - Write descriptive commit messages and add each logical change to a separate commit to make code review easier. 52 | 53 | Make the whole test suite pass (Step 2), and check the final rendering (Step 3). 54 | 55 | 56 | ## Step 5. Push and [submit a Pull Request](https://github.com/dodie/scott/compare/) 57 | Done! Now it's my turn to review your PR and get in touch with you. :) 58 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/ScottConfigurer.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 7 | 8 | public class ScottConfigurer { 9 | 10 | private ScottConfigurer() { 11 | // Utility class, use static methods instead of instantiating this class. 12 | } 13 | 14 | public static Configuration getConfiguration() { 15 | return new Configuration.Builder() 16 | .setTrackerClass("hu.advancedweb.scott.runtime.track.StateRegistry") 17 | .setIncludeLambdasOnlyWhenOtherInstrumentationIsInPlace(true) 18 | .setIncludeByAnnotation(getPropertyConfig("scott.track.method_annotation", 19 | new String[] { "org.junit.Test", "org.junit.jupiter.api.Test", "org.junit.jupiter.api.TestFactory", 20 | "io.cucumber.java" })) 21 | .setInjectJUnit4RuleWhenAnnotationFound(getPropertyConfig( 22 | "scott.inject_junit4_rule.method_annotation", new String[] { "org.junit.Test" })) 23 | .setInjectJUnit5ExtensionWhenAnnotationFound(getPropertyConfig( 24 | "scott.inject_junit5_extension.method_annotation", 25 | new String[] { "org.junit.jupiter.api.Test", "org.junit.jupiter.api.TestFactory" })) 26 | .setTrackReturn(false) 27 | .setTrackUnhandledException(false) 28 | .setVerboseLogging("true".equalsIgnoreCase(System.getenv("scottDebug"))) 29 | .build(); 30 | } 31 | 32 | private static List getPropertyConfig(String propertyKey, final String[] defaultValues) { 33 | final String property = System.getProperty(propertyKey); 34 | 35 | final String[] params; 36 | if (property != null) { 37 | params = property.split(","); 38 | } else { 39 | params = defaultValues; 40 | } 41 | 42 | /* 43 | * Support backward compatibility. 44 | * Previously the packages are had to be specified with wildcards ad the end. (E.g. 'hu.awm.*'.) 45 | * This is not the case anymore, so if a config element ends with '.*', we simply discard these two characters. 46 | */ 47 | for (int i = 0; i < params.length; i++) { 48 | String param = params[i]; 49 | if (param.endsWith(".*")) { 50 | params[i] = param.substring(0, param.length() - 2); 51 | } 52 | } 53 | 54 | return Arrays.asList(params); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /scott-examples/junit5/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | scott-example-junit5 6 | 0.0.1-SNAPSHOT 7 | 8 | 9 | UTF-8 10 | 11 | 12 | 13 | 14 | 15 | hu.advancedweb 16 | scott-maven-plugin 17 | 4.0.1 18 | 19 | 20 | 21 | prepare-agent 22 | 23 | 24 | 25 | 26 | 27 | maven-surefire-plugin 28 | 2.22.2 29 | 30 | 31 | org.junit.platform 32 | junit-platform-surefire-provider 33 | 1.3.2 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-compiler-plugin 40 | 3.8.1 41 | 42 | 17 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-surefire-plugin 48 | 49 | ${argLine} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.junit.jupiter 58 | junit-jupiter-engine 59 | 5.8.0 60 | test 61 | 62 | 63 | org.mockito 64 | mockito-core 65 | 3.12.4 66 | test 67 | 68 | 69 | hu.advancedweb 70 | scott 71 | 4.0.1 72 | test 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /scott-gradle-plugin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /scott-examples/junit4/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /scott-examples/junit5/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/VariableType.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | import org.objectweb.asm.Opcodes; 4 | import org.objectweb.asm.Type; 5 | 6 | /** 7 | * Opcodes and descriptions for each variable type. 8 | * @author David Csakvari 9 | */ 10 | enum VariableType { 11 | INTEGER(Opcodes.ILOAD, Opcodes.ISTORE, Opcodes.IRETURN, "I"), 12 | LONG(Opcodes.LLOAD, Opcodes.LSTORE, Opcodes.LRETURN, "J"), 13 | FLOAT(Opcodes.FLOAD, Opcodes.FSTORE, Opcodes.FRETURN, "F"), 14 | DOUBLE(Opcodes.DLOAD, Opcodes.DSTORE, Opcodes.DRETURN, "D"), 15 | REFERENCE(Opcodes.ALOAD, Opcodes.ASTORE, Opcodes.ARETURN, "Ljava/lang/Object;"), 16 | SHORT(Opcodes.ILOAD, Opcodes.ISTORE, Opcodes.IRETURN, "S"), 17 | BYTE(Opcodes.ILOAD, Opcodes.ISTORE, Opcodes.IRETURN, "B"), 18 | CHAR(Opcodes.ILOAD, Opcodes.ISTORE, Opcodes.IRETURN, "C"), 19 | BOOLEAN(Opcodes.ILOAD, Opcodes.ISTORE, Opcodes.IRETURN, "Z"), 20 | VOID(-1, -1, Opcodes.RETURN, "V") 21 | ; 22 | 23 | final int loadOpcode; 24 | final int storeOpcode; 25 | final int returnOpcode; 26 | final String desc; 27 | 28 | VariableType(int loadOpcode, int storeOpcode, int returnOpcode, String desc) { 29 | this.loadOpcode = loadOpcode; 30 | this.storeOpcode = storeOpcode; 31 | this.returnOpcode = returnOpcode; 32 | this.desc = desc; 33 | } 34 | 35 | static boolean isStoreOperation(final int opcode) { 36 | for (VariableType variableType : VariableType.values()) { 37 | if (variableType.storeOpcode == opcode) { 38 | return true; 39 | } 40 | } 41 | return false; 42 | } 43 | 44 | static boolean isReturnOperation(final int opcode) { 45 | for (VariableType variableType : VariableType.values()) { 46 | if (variableType.returnOpcode == opcode) { 47 | return true; 48 | } 49 | } 50 | return false; 51 | } 52 | 53 | static VariableType getByReturnOpCode(final int opcode) { 54 | for (VariableType variableType : VariableType.values()) { 55 | if (variableType.returnOpcode == opcode) { 56 | return variableType; 57 | } 58 | } 59 | return null; 60 | } 61 | 62 | static VariableType getReturnTypeFromMethodDesc(final String methodDesc) { 63 | return getByDesc(Type.getReturnType(methodDesc).getDescriptor()); 64 | } 65 | 66 | static VariableType getByDesc(final String desc) { 67 | if (desc.startsWith("L") || desc.startsWith("[")) { 68 | return VariableType.REFERENCE; 69 | } else { 70 | for (VariableType variableType : VariableType.values()) { 71 | if (variableType.desc.equals(desc)) { 72 | return variableType; 73 | } 74 | } 75 | } 76 | return null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/ScottClassTransformer.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 4 | import hu.advancedweb.scott.instrumentation.transformation.param.InstrumentationActions; 5 | import org.objectweb.asm.ClassReader; 6 | import org.objectweb.asm.ClassVisitor; 7 | import org.objectweb.asm.ClassWriter; 8 | 9 | import hu.advancedweb.scott.instrumentation.transformation.param.DiscoveryClassVisitor; 10 | 11 | 12 | /** 13 | * Entry point for Scott's ASM based bytecode instrumentation. 14 | * 15 | * @author David Csakvari 16 | */ 17 | public class ScottClassTransformer { 18 | 19 | /** 20 | * Instrument the given class based on the configuration. 21 | * @param classfileBuffer class to be instrumented 22 | * @param configuration configuration settings 23 | * @return instrumented class 24 | */ 25 | public byte[] transform(byte[] classfileBuffer, Configuration configuration) { 26 | InstrumentationActions instrumentationActions = calculateTransformationParameters(classfileBuffer, configuration); 27 | if (!instrumentationActions.includeClass) { 28 | return classfileBuffer; 29 | } 30 | return transform(classfileBuffer, instrumentationActions); 31 | } 32 | 33 | /** 34 | * Based on the structure of the class and the supplied configuration, determine 35 | * the concrete instrumentation actions for the class. 36 | * @param classfileBuffer class to be analyzed 37 | * @param configuration configuration settings 38 | * @return instrumentation actions to be applied 39 | */ 40 | private InstrumentationActions calculateTransformationParameters(byte[] classfileBuffer, Configuration configuration) { 41 | DiscoveryClassVisitor discoveryClassVisitor = new DiscoveryClassVisitor(configuration); 42 | new ClassReader(classfileBuffer).accept(discoveryClassVisitor, 0); 43 | return discoveryClassVisitor.getTransformationParameters().build(); 44 | } 45 | 46 | /** 47 | * Perform the given instrumentation actions on a class. 48 | * @param classfileBuffer class to be transformed 49 | * @param instrumentationActions instrumentation actions to be applied 50 | * @return instrumented class 51 | */ 52 | private byte[] transform(byte[] classfileBuffer, InstrumentationActions instrumentationActions) { 53 | ClassReader classReader = new ClassReader(classfileBuffer); 54 | ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES); 55 | ClassVisitor localVariableStateEmitterTestClassVisitor = new StateTrackingClassVisitor(classWriter, 56 | instrumentationActions); 57 | classReader.accept(localVariableStateEmitterTestClassVisitor, 0); 58 | return classWriter.toByteArray(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/FieldRecordingInitialValuesTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.hamcrest.CoreMatchers.equalTo; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import org.junit.Test; 7 | 8 | public class FieldRecordingInitialValuesTest { 9 | 10 | int a = 10; 11 | int b = 11; 12 | static int A = 12; 13 | static int B = 13; 14 | 15 | 16 | @Test 17 | public void recordFieldAccess() throws Exception { 18 | // track initial states of accessed fields 19 | assertThat(TestHelper.getLastRecordedStateForField("this.a"), equalTo("10")); 20 | assertThat(TestHelper.getLastRecordedStateForField("this.b"), equalTo("11")); 21 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.A"), equalTo("12")); 22 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.B"), equalTo("13")); 23 | 24 | @SuppressWarnings("unused") 25 | String accessed = "" + a + b + A + B; 26 | } 27 | 28 | @Test 29 | public void recordFieldSubsetAccess() throws Exception { 30 | // track initial states of accessed fields 31 | assertThat(TestHelper.getLastRecordedStateForField("this.a"), equalTo("10")); 32 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.B"), equalTo("13")); 33 | 34 | // don't record not accessed fields 35 | assertThat(TestHelper.getLastRecordedStateForField("b"), equalTo(null)); 36 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.A"), equalTo(null)); 37 | 38 | @SuppressWarnings("unused") 39 | String accessed = "" + a + B; 40 | } 41 | 42 | @Test 43 | public void recordWrite() throws Exception { 44 | // initial value match 45 | assertThat(TestHelper.getLastRecordedStateForField("this.a"), equalTo("10")); 46 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.A"), equalTo("12")); 47 | 48 | a = 20; 49 | A = 22; 50 | 51 | // values after write match 52 | assertThat(TestHelper.getLastRecordedStateForField("this.a"), equalTo("20")); 53 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.A"), equalTo("22")); 54 | } 55 | 56 | @Test 57 | public void recordReadAndWrite() throws Exception { 58 | // initial modified values match 59 | assertThat(TestHelper.getLastRecordedStateForField("this.b"), equalTo("11")); 60 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.B"), equalTo("13")); 61 | 62 | @SuppressWarnings("unused") 63 | String accessed = "" + b + B; 64 | 65 | // no change after reading 66 | assertThat(TestHelper.getLastRecordedStateForField("this.b"), equalTo("11")); 67 | assertThat(TestHelper.getLastRecordedStateForField("FieldRecordingInitialValuesTest.B"), equalTo("13")); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | junit4-tests 6 | 4.0.1 7 | 8 | 9 | UTF-8 10 | 11 | 12 | Scott Test Reporter - JUnit 4 Test Suite 13 | 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-dependency-plugin 21 | 22 | 23 | copy-agent 24 | process-test-classes 25 | 26 | copy 27 | 28 | 29 | 30 | 31 | hu.advancedweb 32 | scott 33 | ${project.build.directory}/agents 34 | scott-agent.jar 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | org.apache.maven.plugins 47 | maven-surefire-plugin 48 | 2.22.2 49 | 50 | -javaagent:${project.build.directory}/agents/scott-agent.jar 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.8.1 58 | 59 | 1.8 60 | 1.8 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | junit 69 | junit 70 | 4.13.2 71 | test 72 | 73 | 74 | org.mockito 75 | mockito-core 76 | 3.12.4 77 | test 78 | 79 | 80 | hu.advancedweb 81 | scott 82 | ${project.version} 83 | test 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/report/javasource/MethodSource.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.report.javasource; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import com.github.javaparser.JavaParser; 14 | import com.github.javaparser.ast.CompilationUnit; 15 | 16 | import hu.advancedweb.scott.runtime.report.javasource.MethodBoundaryExtractor.Bounderies; 17 | 18 | /** 19 | * Contains the source code of a test method. 20 | * 21 | * @author David Csakvari 22 | */ 23 | public class MethodSource { 24 | 25 | private final String path; 26 | private final String methodName; 27 | private int beginLine; 28 | private List reportLines = new ArrayList<>(); 29 | private String className; 30 | 31 | public MethodSource(String className, String methodName) throws IOException { 32 | if (className == null || methodName == null) { 33 | throw new NullPointerException(); 34 | } 35 | 36 | final String containerClassFileName; 37 | if (className.contains("$")) { 38 | containerClassFileName = className.substring(0, className.indexOf('$')); 39 | } else { 40 | containerClassFileName = className; 41 | } 42 | 43 | final String scopedClassName; 44 | if (className.contains(".")) { 45 | scopedClassName = className.substring(className.lastIndexOf('.') + 1); 46 | } else { 47 | scopedClassName = className; 48 | } 49 | 50 | this.path = new SourcePathResolver().getSourcePath(containerClassFileName); 51 | this.className = className; 52 | this.methodName = methodName; 53 | 54 | CompilationUnit cu = getCompilationUnit(path); 55 | 56 | MethodBoundaryExtractor visitor = new MethodBoundaryExtractor(scopedClassName, methodName); 57 | final Bounderies boundary = new Bounderies(); 58 | visitor.visit(cu, boundary); 59 | 60 | beginLine = boundary.beginLine; 61 | 62 | List lines = Files.readAllLines(Paths.get(path),StandardCharsets.UTF_8); 63 | for (int i = boundary.beginLine - 1; i < boundary.endLine; i++) { 64 | reportLines.add(lines.get(i)); 65 | } 66 | } 67 | 68 | private CompilationUnit getCompilationUnit(String testSourcePath) throws IOException { 69 | try(InputStream in = new FileInputStream(new File(testSourcePath))) { 70 | return new JavaParser().parse(in).getResult().get(); 71 | } 72 | } 73 | 74 | public String getPath() { 75 | return path; 76 | } 77 | 78 | public String getClassName() { 79 | return className; 80 | } 81 | 82 | public String getMethodName() { 83 | return methodName; 84 | } 85 | 86 | public int getBeginLine() { 87 | return beginLine; 88 | } 89 | 90 | public List getReportLines() { 91 | return reportLines; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /scott-examples/cucumber-io-cucumber/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | scott-example-cucumber2 6 | 0.0.1-SNAPSHOT 7 | 8 | 9 | UTF-8 10 | 11 | 12 | 13 | 14 | 15 | hu.advancedweb 16 | scott-maven-plugin 17 | 4.0.1 18 | 19 | 20 | 21 | prepare-agent 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-surefire-plugin 29 | 2.22.2 30 | 31 | 32 | net.masterthought 33 | maven-cucumber-reporting 34 | 5.5.4 35 | 36 | 37 | execution 38 | verify 39 | 40 | generate 41 | 42 | 43 | Test Result 44 | ${project.build.directory}/cucumber-report-html 45 | 46 | 47 | 48 | 49 | 50 | org.apache.maven.plugins 51 | maven-compiler-plugin 52 | 3.8.1 53 | 54 | 1.7 55 | 1.7 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | io.cucumber 64 | cucumber-java 65 | 6.11.0 66 | 67 | 68 | io.cucumber 69 | cucumber-junit 70 | 6.11.0 71 | test 72 | 73 | 74 | 75 | junit 76 | junit 77 | 4.13.2 78 | test 79 | 80 | 81 | org.hamcrest 82 | hamcrest-library 83 | 1.3 84 | test 85 | 86 | 87 | hu.advancedweb 88 | scott 89 | 4.0.1 90 | test 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /scott-maven-plugin/src/hu/advancedweb/maven/ScottAgentMojo.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.maven; 2 | 3 | import org.apache.maven.artifact.Artifact; 4 | import org.apache.maven.plugin.AbstractMojo; 5 | import org.apache.maven.plugin.MojoExecutionException; 6 | import org.apache.maven.plugin.MojoFailureException; 7 | import org.apache.maven.plugins.annotations.LifecyclePhase; 8 | import org.apache.maven.plugins.annotations.Mojo; 9 | import org.apache.maven.plugins.annotations.Parameter; 10 | import org.apache.maven.plugins.annotations.ResolutionScope; 11 | import org.apache.maven.project.MavenProject; 12 | 13 | import java.io.File; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Properties; 17 | 18 | import static java.lang.String.format; 19 | import static org.codehaus.plexus.util.StringUtils.join; 20 | 21 | @Mojo(name = "prepare-agent", defaultPhase = LifecyclePhase.INITIALIZE, 22 | requiresDependencyResolution = ResolutionScope.RUNTIME, 23 | threadSafe = true) 24 | public class ScottAgentMojo extends AbstractMojo { 25 | 26 | private static final String SCOTT_ARTIFACT_NAME = "hu.advancedweb:scott"; 27 | private static final String ARG_LINE = "argLine"; 28 | 29 | @Parameter(property = "project", readonly = true) 30 | private MavenProject project; 31 | 32 | @Parameter(property = "plugin.artifactMap", required = true, readonly = true) 33 | Map pluginArtifactMap; 34 | 35 | @Parameter(property = "scott.track.method_annotation") 36 | List trackMethodAnnotations; 37 | 38 | @Parameter(property = "scott.inject_junit4_rule.method_annotation") 39 | List junit4RuleMethodAnnotations; 40 | 41 | @Parameter(property = "scott.inject_junit5_extension.method_annotation") 42 | List junit5RuleMethodAnnotations; 43 | 44 | @Override 45 | public void execute() throws MojoExecutionException, MojoFailureException { 46 | final Properties projectProperties = this.project.getProperties(); 47 | final File agentJarFile = pluginArtifactMap.get(SCOTT_ARTIFACT_NAME).getFile(); 48 | String arguments = getArgument(agentJarFile); 49 | getLog().info(ARG_LINE + " set to " + arguments); 50 | projectProperties.setProperty(ARG_LINE, arguments); 51 | } 52 | 53 | private String getArgument(final File agentJarFile) { 54 | return format("-javaagent:%s", agentJarFile) 55 | + createScottArgument("scott.track.method_annotation", trackMethodAnnotations) 56 | + createScottArgument("scott.inject_junit4_rule.method_annotation", junit4RuleMethodAnnotations) 57 | + createScottArgument("scott.inject_junit5_extension.method_annotation", junit5RuleMethodAnnotations); 58 | } 59 | 60 | private String createScottArgument(String name, List arguments) { 61 | if (arguments == null || arguments.isEmpty()) 62 | return ""; 63 | 64 | return format(" -D%s=\"%s\"", name, join(arguments.iterator(), ",")); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/runtime/report/FailureRendererTest.java: -------------------------------------------------------------------------------- 1 | 2 | package hu.advancedweb.scott.runtime.report; 3 | 4 | import org.junit.Test; 5 | 6 | import static org.hamcrest.CoreMatchers.containsString; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | 9 | public class FailureRendererTest { 10 | 11 | @Test 12 | public void verifyPlusPus() { 13 | int i = 0; 14 | while (i < 10) { 15 | i++; 16 | } 17 | String resultText = FailureRenderer.render("xxxxx", "yyyyy", new RuntimeException("bar")); 18 | assertPlusPlus(resultText); 19 | } 20 | 21 | private void assertPlusPlus(String resultText) { 22 | assertThat("variable loop", resultText, containsString("i=1;2;3...;8;9;[10] =>10")); 23 | } 24 | 25 | @Test 26 | public void verifySingleInt() { 27 | @SuppressWarnings("unused") 28 | int i = 42; 29 | String resultText = FailureRenderer.render("xxxxx", "yyyyy", new RuntimeException("bar")); 30 | assertSingleInt(resultText); 31 | } 32 | 33 | private void assertSingleInt(String resultText) { 34 | assertThat("variable loop", resultText, containsString("i=42")); 35 | } 36 | 37 | @Test 38 | public void verifyPlusTwo() { 39 | int i = 0; 40 | while (i < 10) { 41 | i+=2; 42 | } 43 | String resultText = FailureRenderer.render("xxxxx", "yyyyy", new RuntimeException("bar")); 44 | assertPlusTwo(resultText); 45 | } 46 | 47 | private void assertPlusTwo(String resultText) { 48 | assertThat("variable loop", resultText, containsString("i=2;4;6;8;[10]")); 49 | } 50 | 51 | @Test 52 | public void verifyNestedPlusPlus() { 53 | int i = 0; 54 | int j = 0; 55 | while (i < 10) { 56 | i++; 57 | while (j < 10) { 58 | j++; 59 | } 60 | } 61 | String resultText = FailureRenderer.render("xxxxx", "yyyyy", new RuntimeException("bar")); 62 | assertNestedPlusPlus(resultText); 63 | } 64 | 65 | private void assertNestedPlusPlus(String resultText) { 66 | assertThat("variable loop", resultText, containsString("i=1;2;3...;8;9;[10] =>10")); 67 | assertThat("variable loop", resultText, containsString("j=1;2;3...;8;9;[10] =>10")); 68 | } 69 | 70 | 71 | @Test 72 | public void verifyNestedPlusTwo() { 73 | int i = 0; 74 | int j = 0; 75 | while (i < 10) { 76 | i++; 77 | while (j < 10) { 78 | j+=2; 79 | } 80 | } 81 | String resultText = FailureRenderer.render("xxxxx", "yyyyy", new RuntimeException("bar")); 82 | assertNestedPlusTwo(resultText); 83 | } 84 | 85 | private void assertNestedPlusTwo(String resultText) { 86 | assertThat("variable loop", resultText, containsString("i=1;2;3...;8;9;[10] =>10")); 87 | assertThat("variable loop", resultText, containsString("j=2;4;6;8;[10]")); 88 | } 89 | } -------------------------------------------------------------------------------- /scott-tests/junit5-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | hu.advancedweb 5 | junit5-tests 6 | 4.0.1 7 | 8 | 9 | UTF-8 10 | 11 | 12 | Scott Test Reporter - JUnit 5 Test Suite 13 | 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-dependency-plugin 21 | 22 | 23 | copy-agent 24 | process-test-classes 25 | 26 | copy 27 | 28 | 29 | 30 | 31 | hu.advancedweb 32 | scott 33 | ${project.build.directory}/agents 34 | scott-agent.jar 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | maven-surefire-plugin 47 | 2.22.2 48 | 49 | 50 | org.junit.platform 51 | junit-platform-surefire-provider 52 | 1.3.2 53 | 54 | 55 | 56 | -javaagent:${project.build.directory}/agents/scott-agent.jar 57 | 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-compiler-plugin 63 | 3.8.1 64 | 65 | 17 66 | 17 67 | 17 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.junit.jupiter 76 | junit-jupiter-engine 77 | 5.8.0 78 | test 79 | 80 | 81 | org.mockito 82 | mockito-core 83 | 3.12.4 84 | test 85 | 86 | 87 | hu.advancedweb 88 | scott 89 | ${project.version} 90 | test 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /scott-examples/junit5/src/test/java/hu/advancedweb/scott/JUnit5DemoTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static java.time.Duration.ofMillis; 4 | import static java.time.Duration.ofMinutes; 5 | import static org.junit.jupiter.api.Assertions.assertAll; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTimeout; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | import org.junit.jupiter.api.DisplayName; 18 | import org.junit.jupiter.api.Test; 19 | 20 | 21 | @SuppressWarnings("unused") 22 | @DisplayName("Test with JUnit 5 assertions") 23 | public class JUnit5DemoTest { 24 | 25 | @Test 26 | public void testWithMessageSupplier() { 27 | String first = "Hello"; 28 | String last = "World"; 29 | 30 | String concatenated = first + " " + last; 31 | 32 | assertEquals("Goodbye World", concatenated, 33 | () -> "Incorrect message."); 34 | } 35 | 36 | @Test 37 | @DisplayName("This is a test with multiple assertions.") 38 | public void testWithGroupedAssertions() { 39 | Integer[] myArray = new Integer[] { 1, 4, 2, 4 }; 40 | List myList = Arrays.asList(myArray); 41 | 42 | Set mySet = new HashSet<>(myList); 43 | mySet.remove(4); 44 | 45 | assertAll("mySet is too small", 46 | () -> assertTrue(mySet.size() > 2, "It does not contain a whole lot of numbers!"), 47 | () -> assertTrue(mySet.contains(4), "It does not even contain 4!")); 48 | } 49 | 50 | @Test 51 | @DisplayName("😱") 52 | void exceptionTesting() { 53 | assertThrows(NullPointerException.class, () -> { 54 | List set = new ArrayList<>(); 55 | set.add("I"); 56 | set.add("will"); 57 | set.add("not"); 58 | set.add("explode!"); 59 | assertTrue(set.size() > 1); 60 | }); 61 | } 62 | 63 | @Test 64 | void timeoutExceedingTest() { 65 | assertTimeout(ofMillis(2), () -> { 66 | String calculate = "slow"; 67 | calculate += "operation"; 68 | Thread.sleep(1000L); 69 | }); 70 | } 71 | 72 | @Test 73 | void timeoutNotExceededWithResult() { 74 | String actualResult = assertTimeout(ofMinutes(2), () -> { 75 | return "result"; 76 | }); 77 | assertEquals("no result", actualResult); 78 | } 79 | 80 | @Test 81 | public void testWithTextBlock() { 82 | String textBlock = """ 83 | Hello, 84 | this is a multi-line text block. 85 | """; 86 | 87 | assertEquals("Is this a text block?", textBlock); 88 | } 89 | 90 | @Test 91 | public void testWithSwitchExpression() { 92 | Day day = Day.WEDNESDAY; 93 | int j = switch (day) { 94 | case MONDAY -> 0; 95 | case TUESDAY -> 1; 96 | default -> { 97 | int k = day.toString().length(); 98 | yield k; 99 | } 100 | }; 101 | 102 | assertEquals(0, j); 103 | } 104 | 105 | private enum Day { 106 | MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/StateTrackingClassVisitor.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation; 2 | 3 | import org.junit.Rule; 4 | import org.objectweb.asm.AnnotationVisitor; 5 | import org.objectweb.asm.ClassVisitor; 6 | import org.objectweb.asm.FieldVisitor; 7 | import org.objectweb.asm.MethodVisitor; 8 | import org.objectweb.asm.Opcodes; 9 | import org.objectweb.asm.Type; 10 | 11 | import hu.advancedweb.scott.instrumentation.transformation.param.InstrumentationActions; 12 | import hu.advancedweb.scott.runtime.ScottReportingRule; 13 | 14 | /** 15 | * Manitpulates the class through the appropriate MethodVisitors. 16 | * 17 | * @see ConstructorTransformerMethodVisitor 18 | * @see ScopeExtractorMethodVisitor 19 | * @see StateTrackingMethodVisitor 20 | * @author David Csakvari 21 | */ 22 | public class StateTrackingClassVisitor extends ClassVisitor { 23 | 24 | private String className; 25 | private InstrumentationActions instrumentationActions; 26 | 27 | public StateTrackingClassVisitor(ClassVisitor cv, InstrumentationActions instrumentationActions) { 28 | super(Opcodes.ASM9, cv); 29 | this.instrumentationActions = instrumentationActions; 30 | } 31 | 32 | @Override 33 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 34 | this.className = name; 35 | super.visit(version, access, name, signature, superName, interfaces); 36 | } 37 | 38 | @Override 39 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 40 | MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions); 41 | 42 | if (name.equals("")) { 43 | if (instrumentationActions.isJUnit4RuleInjectionRequired) { 44 | return new ConstructorTransformerMethodVisitor(methodVisitor, access, name, desc, signature, exceptions, className); 45 | } else { 46 | return methodVisitor; 47 | } 48 | } else if (instrumentationActions.isMethodTrackingRequired(name, desc, signature)) { 49 | StateTrackingMethodVisitor variableMutationEventEmitter = new StateTrackingMethodVisitor(methodVisitor, instrumentationActions, className, name, desc); 50 | return new ScopeExtractorMethodVisitor(variableMutationEventEmitter, access, name, desc, signature, exceptions); 51 | } else { 52 | return methodVisitor; 53 | } 54 | } 55 | 56 | @Override 57 | public void visitEnd() { 58 | if (instrumentationActions.isJUnit4RuleInjectionRequired) { 59 | FieldVisitor fv = super.visitField(Opcodes.ACC_PUBLIC, "scottReportingRule", Type.getDescriptor(ScottReportingRule.class), null, null); 60 | fv.visitAnnotation(Type.getDescriptor(Rule.class), true).visitEnd(); 61 | } 62 | 63 | if (instrumentationActions.isJUnit5ExtensionInjectionRequired) { 64 | AnnotationVisitor av0 = super.visitAnnotation("Lorg/junit/jupiter/api/extension/ExtendWith;", true); 65 | AnnotationVisitor av1 = av0.visitArray("value"); 66 | av1.visit(null, Type.getType("Lhu/advancedweb/scott/runtime/ScottJUnit5Extension;")); 67 | av1.visitEnd(); 68 | av0.visitEnd(); 69 | } 70 | 71 | super.visitEnd(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/helper/InstrumentedObject.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.helper; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Paths; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import hu.advancedweb.scott.instrumentation.transformation.ScottClassTransformer; 11 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 12 | 13 | /** 14 | * Dynamically loads and instruments a class from the configuration-example-classes module based on the given FQN. 15 | * If the class requires additional classes from the project it can be specified as dependencies. 16 | * After the instrumentation an instance will be created from the class using it's default constructor. 17 | * 18 | * @author David Csakvari 19 | */ 20 | public class InstrumentedObject { 21 | 22 | private static final String BASE_PATH = System.getProperty("user.dir") + File.separator + ".." + File.separator + "configuration-example-classes" + File.separator + "target" + File.separator + "classes" + File.separator; 23 | 24 | private final Class clazz; 25 | private final Object instance; 26 | 27 | public InstrumentedObject(Class clazz, Object instance) { 28 | this.clazz = clazz; 29 | this.instance = instance; 30 | } 31 | 32 | public static InstrumentedObject create(String fqn, Configuration configuration, String... dependencies) { 33 | try { 34 | byte[] originalClass = getBytes(fqn); 35 | byte[] instrumentedClass = new ScottClassTransformer().transform(originalClass, configuration); 36 | 37 | try { 38 | Map classBytecodes = new HashMap<>(); 39 | classBytecodes.put(fqn, instrumentedClass); 40 | 41 | for (String dependency : dependencies) { 42 | classBytecodes.put(dependency, getBytes(dependency)); 43 | } 44 | 45 | ByteArrayClassLoader ccl = new ByteArrayClassLoader(Thread.currentThread().getContextClassLoader(), classBytecodes); 46 | Class clazz = ccl.loadClass(fqn); 47 | Object instance = clazz.getDeclaredConstructor().newInstance(); 48 | 49 | return new InstrumentedObject(clazz, instance); 50 | } catch (Throwable e) { 51 | if (e instanceof VerifyError) { 52 | printBytecode(originalClass, instrumentedClass); 53 | } 54 | throw e; 55 | } 56 | } catch (Exception e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | 61 | public static byte[] getBytes(String name) throws IOException { 62 | String relativeClassFilePath = name.replaceAll("\\.", File.separator) + ".class"; 63 | return Files.readAllBytes(Paths.get(BASE_PATH + relativeClassFilePath)); 64 | } 65 | 66 | private static void printBytecode(byte[] originalClass, byte[] instrumentedClass) { 67 | System.out.println(""); 68 | System.out.println("Original Class"); 69 | ClassFileStructurePrinter.viewByteCode(originalClass); 70 | System.out.println(""); 71 | 72 | System.out.println(""); 73 | System.out.println("Instrumented Class"); 74 | ClassFileStructurePrinter.viewByteCode(instrumentedClass); 75 | System.out.println(""); 76 | } 77 | 78 | public Object invokeMethod(String methodName) throws Exception { 79 | return clazz.getDeclaredMethod(methodName).invoke(instance); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /scott/src/test/java/hu/advancedweb/scott/instrumentation/transformation/config/ClassMatcherTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation.config; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.assertFalse; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | 11 | import org.junit.Test; 12 | 13 | public class ClassMatcherTest { 14 | 15 | @Test 16 | public void test_matchesAsClass() { 17 | assertTrue(ClassMatcher.matchesAsClass("hu.awm.MyClass", Collections.singletonList("hu.awm.MyClass"))); 18 | assertTrue(ClassMatcher.matchesAsClass("hu.awm.MyClass", Arrays.asList("hu.awm.SomethingElse", "hu.awm.MyClass"))); 19 | assertFalse(ClassMatcher.matchesAsClass("hu.awm.MyClass$InnerClass", Collections.singletonList("hu.awm.MyClass"))); 20 | assertFalse(ClassMatcher.matchesAsClass("hu.awm.other.package.MyClass", Collections.singletonList("hu.awm.MyClass"))); 21 | assertFalse(ClassMatcher.matchesAsClass("hu.awm.MyClass", Collections.singletonList("hu.awm.SomethingElse"))); 22 | assertFalse(ClassMatcher.matchesAsClass("hu.awm.MyClass", new ArrayList())); 23 | } 24 | 25 | @Test 26 | public void test_matchesAsInnerClass() { 27 | assertTrue(ClassMatcher.matchesAsInnerClass("hu.awm.MyClass$InnerClass", Collections.singletonList("hu.awm.MyClass"))); 28 | assertTrue(ClassMatcher.matchesAsInnerClass("hu.awm.MyClass$InnerClass$InnerInnerClass", Collections.singletonList("hu.awm.MyClass"))); 29 | assertTrue(ClassMatcher.matchesAsInnerClass("hu.awm.MyClass$InnerClass", Arrays.asList("hu.awm.SomethingElse", "hu.awm.MyClass"))); 30 | assertFalse(ClassMatcher.matchesAsInnerClass("hu.awm.MyClass", Collections.singletonList("hu.awm.MyClass"))); 31 | assertFalse(ClassMatcher.matchesAsInnerClass("hu.awm.other.package.MyClass$InnerClass", Collections.singletonList("hu.awm.MyClass"))); 32 | assertFalse(ClassMatcher.matchesAsInnerClass("hu.awm.MyClass$InnerClass", Collections.singletonList("hu.awm.SomethingElse"))); 33 | assertFalse(ClassMatcher.matchesAsInnerClass("hu.awm.MyClass$InnerClass", new ArrayList())); 34 | } 35 | 36 | @Test 37 | public void test_matchesAsPackage() { 38 | assertTrue(ClassMatcher.matchesAsPackage("hu.awm.MyClass", Collections.singletonList("hu.awm"))); 39 | assertTrue(ClassMatcher.matchesAsPackage("hu.awm.MyClass$InnerClass", Collections.singletonList("hu.awm"))); 40 | assertTrue(ClassMatcher.matchesAsPackage("hu.awm.MyClass$InnerClass$InnerInnerClass", Collections.singletonList("hu.awm"))); 41 | assertTrue(ClassMatcher.matchesAsPackage("hu.awm.MyClass", Arrays.asList("hu.something.else", "hu.awm"))); 42 | assertFalse(ClassMatcher.matchesAsPackage("hu.something.else.MyClass$InnerClass", Collections.singletonList("hu.awm"))); 43 | assertFalse(ClassMatcher.matchesAsPackage("hu.awm.MyClass", Collections.singletonList("hu.something.else"))); 44 | assertFalse(ClassMatcher.matchesAsPackage("hu.awm.MyClass", new ArrayList())); 45 | } 46 | 47 | @Test 48 | public void test_anyMatchesAsClassOrPackage() { 49 | assertTrue(ClassMatcher.anyMatchesAsClassOrPackage(Arrays.asList("hu.awm.MyClass"), Collections.singletonList("hu.awm"))); 50 | assertTrue(ClassMatcher.anyMatchesAsClassOrPackage(Arrays.asList("hu.awm.MyClass$InnerClass"), Collections.singletonList("hu.awm.MyClass"))); 51 | assertTrue(ClassMatcher.anyMatchesAsClassOrPackage(Arrays.asList("hu.awm.MyClass"), Collections.singletonList("hu.awm.MyClass"))); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/param/DiscoveryClassVisitor.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation.param; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 8 | import org.objectweb.asm.AnnotationVisitor; 9 | import org.objectweb.asm.ClassVisitor; 10 | import org.objectweb.asm.MethodVisitor; 11 | import org.objectweb.asm.Opcodes; 12 | import org.objectweb.asm.Type; 13 | 14 | /** 15 | * Determines what classes and methods to instrument. 16 | * 17 | * @author David Csakvari 18 | */ 19 | public class DiscoveryClassVisitor extends ClassVisitor { 20 | 21 | private Configuration configuration; 22 | 23 | private InstrumentationActions.Builder transformationParameters; 24 | 25 | private String classFqn; 26 | private List annotationFqns = new ArrayList<>(); 27 | 28 | 29 | public DiscoveryClassVisitor(Configuration configuration) { 30 | super(Opcodes.ASM9); 31 | this.configuration = configuration; 32 | this.transformationParameters = new InstrumentationActions.Builder(); 33 | } 34 | 35 | public InstrumentationActions.Builder getTransformationParameters() { 36 | return transformationParameters; 37 | } 38 | 39 | @Override 40 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 41 | this.classFqn = Type.getType("L" + name + ";").getClassName(); 42 | super.visit(version, access, name, signature, superName, interfaces); 43 | } 44 | 45 | @Override 46 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 47 | annotationFqns.add(Type.getType(descriptor).getClassName()); 48 | return super.visitAnnotation(descriptor, visible); 49 | } 50 | 51 | @Override 52 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 53 | MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions); 54 | return new DiscoveryMethodVisitor(methodVisitor, transformationParameters, configuration, name, desc, signature, annotationFqns); 55 | } 56 | 57 | @Override 58 | public void visitEnd() { 59 | super.visitEnd(); 60 | transformationParameters.setTrackerClass(configuration.getTrackerClass().replace('.', '/')); 61 | transformationParameters.includeClass(configuration.isClassInstrumentationAllowed(classFqn, annotationFqns)); 62 | transformationParameters.trackMethodStart(configuration.isTrackMethodStart()); 63 | transformationParameters.trackReturn(configuration.isTrackReturn()); 64 | transformationParameters.trackUnhandledException(configuration.isTrackUnhandledException()); 65 | transformationParameters.trackLocalVariableAssignments(configuration.isTrackLocalVariableAssignments()); 66 | transformationParameters.trackLocalVariableIncrements(configuration.isTrackLocalVariableIncrements()); 67 | transformationParameters.trackLocalVariablesAfterEveryMethodCall(configuration.isTrackLocalVariablesAfterEveryMethodCall()); 68 | transformationParameters.trackFieldAssignments(configuration.isTrackFieldAssignments()); 69 | transformationParameters.trackFieldsAfterEveryMethodCall(configuration.isTrackFieldsAfterEveryMethodCall()); 70 | transformationParameters.verboseLogging(configuration.isVerboseLoggingEnabled()); 71 | 72 | if (!transformationParameters.anyMethodMarkedForTracking() && 73 | configuration.isLambdaInstrumentationAllowedWhenOtherInstrumentationIsInPlace()) { 74 | transformationParameters.clearTrackedLambdas(); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /scott-tests/junit5-tests/src/test/java/hu/advancedweb/scott/NestedClassTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.helper.TestHelper.wrapped; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import hu.advancedweb.scott.helper.TestHelper; 11 | 12 | class NestedClassTest { 13 | 14 | String value; 15 | 16 | @BeforeEach 17 | void reset() { 18 | value = "1"; 19 | } 20 | 21 | @Test 22 | void test() { 23 | String dot = "."; 24 | value += dot; 25 | assertEquals(wrapped(dot), TestHelper.getLastRecordedStateForVariable("dot")); 26 | assertEquals(wrapped(value), TestHelper.getLastRecordedStateForField("this.value")); 27 | assertEquals("1.", value); 28 | } 29 | 30 | @Nested 31 | class OuterNested { 32 | 33 | String outerNestedValue = "a"; 34 | 35 | @BeforeEach 36 | void reset() { 37 | value = value + "2"; 38 | } 39 | 40 | @Test 41 | void test() { 42 | String dot = "."; 43 | outerNestedValue += dot; 44 | value += dot + outerNestedValue; 45 | 46 | assertEquals(wrapped(dot), TestHelper.getLastRecordedStateForVariable("dot")); 47 | assertEquals(wrapped(value), TestHelper.getLastRecordedStateForField("(in enclosing NestedClassTest) value")); 48 | assertEquals(wrapped(outerNestedValue), TestHelper.getLastRecordedStateForField("this.outerNestedValue")); 49 | assertEquals("12.a.", value); 50 | } 51 | 52 | @Nested 53 | class MiddleNested { 54 | String middleNestedValue = "I"; 55 | 56 | @BeforeEach 57 | void reset() { 58 | value = value + "3"; 59 | } 60 | 61 | @Test 62 | void test() { 63 | String dot = "."; 64 | middleNestedValue += dot; 65 | outerNestedValue += dot; 66 | value += dot + outerNestedValue + middleNestedValue; 67 | 68 | assertEquals(wrapped(dot), TestHelper.getLastRecordedStateForVariable("dot")); 69 | assertEquals(wrapped(value), TestHelper.getLastRecordedStateForField("(in enclosing NestedClassTest) value")); 70 | assertEquals(wrapped(outerNestedValue), TestHelper.getLastRecordedStateForField("(in enclosing OuterNested) outerNestedValue")); 71 | assertEquals(wrapped(middleNestedValue), TestHelper.getLastRecordedStateForField("this.middleNestedValue")); 72 | assertEquals("123.a.I.", value); 73 | } 74 | 75 | @Nested 76 | class InnerNested { 77 | String innerNestedValue = "x"; 78 | 79 | @BeforeEach 80 | void reset() { 81 | value = value + "4"; 82 | } 83 | 84 | @Test 85 | void test() { 86 | String dot = "."; 87 | innerNestedValue += dot; 88 | middleNestedValue += dot; 89 | outerNestedValue += dot; 90 | value += dot + outerNestedValue + middleNestedValue + innerNestedValue; 91 | 92 | assertEquals(wrapped(dot), TestHelper.getLastRecordedStateForVariable("dot")); 93 | assertEquals(wrapped(value), TestHelper.getLastRecordedStateForField("(in enclosing NestedClassTest) value")); 94 | assertEquals(wrapped(outerNestedValue), TestHelper.getLastRecordedStateForField("(in enclosing OuterNested) outerNestedValue")); 95 | assertEquals(wrapped(middleNestedValue), TestHelper.getLastRecordedStateForField("(in enclosing MiddleNested) middleNestedValue")); 96 | assertEquals(wrapped(innerNestedValue), TestHelper.getLastRecordedStateForField("this.innerNestedValue")); 97 | assertEquals("1234.a.I.x.", value); 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/JdkLibTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.TestHelper.wrapped; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Comparator; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.function.BinaryOperator; 13 | import java.util.stream.Collectors; 14 | 15 | import org.junit.Test; 16 | 17 | public class JdkLibTest { 18 | 19 | /* 20 | * These tests ensure that unit tests can call various JDK libs. 21 | */ 22 | 23 | @Test 24 | public void using_streams_dont_crash() throws Exception { 25 | List strings = Arrays.asList("abc", "bc", "", "xyz", "abc"); 26 | 27 | List recollectedStrings = strings.stream().collect(Collectors.toList()); 28 | assertThat(TestHelper.getLastRecordedStateForVariable("recollectedStrings"), equalTo(recollectedStrings.toString())); 29 | 30 | Map> groupedStrings = strings.stream().collect(Collectors.groupingByConcurrent(String::length)); 31 | assertThat(TestHelper.getLastRecordedStateForVariable("groupedStrings"), equalTo(groupedStrings.toString())); 32 | 33 | List filteredStrings = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList()); 34 | assertThat(TestHelper.getLastRecordedStateForVariable("filteredStrings"), equalTo(filteredStrings.toString())); 35 | 36 | List otherStrings = new ArrayList<>(); 37 | strings.forEach(s -> otherStrings.add(s)); 38 | assertThat(TestHelper.getLastRecordedStateForVariable("otherStrings"), equalTo(otherStrings.toString())); 39 | 40 | List doubleStrings = strings.stream().map(s -> s + s).collect(Collectors.toList()); 41 | assertThat(TestHelper.getLastRecordedStateForVariable("doubleStrings"), equalTo(doubleStrings.toString())); 42 | 43 | List parallellyComputedDoubleStrings = strings.parallelStream().map(s -> s + s).collect(Collectors.toList()); 44 | assertThat(TestHelper.getLastRecordedStateForVariable("parallellyComputedDoubleStrings"), equalTo(parallellyComputedDoubleStrings.toString())); 45 | 46 | List distinctStrings = strings.stream().distinct().collect(Collectors.toList()); 47 | assertThat(TestHelper.getLastRecordedStateForVariable("distinctStrings"), equalTo(distinctStrings.toString())); 48 | 49 | List limitedStrings = strings.stream().limit(3).collect(Collectors.toList()); 50 | assertThat(TestHelper.getLastRecordedStateForVariable("limitedStrings"), equalTo(limitedStrings.toString())); 51 | 52 | List sortedStrings = strings.stream().sorted().collect(Collectors.toList()); 53 | assertThat(TestHelper.getLastRecordedStateForVariable("sortedStrings"), equalTo(sortedStrings.toString())); 54 | 55 | String mergedStrings = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", ")); 56 | assertThat(TestHelper.getLastRecordedStateForVariable("mergedStrings"), equalTo(wrapped(mergedStrings.toString()))); 57 | 58 | boolean hasX = strings.stream().anyMatch(s -> s.contains("x")); 59 | assertThat(TestHelper.getLastRecordedStateForVariable("hasX"), equalTo(Boolean.toString(hasX))); 60 | } 61 | 62 | @Test 63 | public void using_binary_operatior_dont_crash() throws Exception { 64 | BinaryOperator.maxBy(new Comparator() { 65 | @Override 66 | public int compare(String o1, String o2) { 67 | return 0; // Does not matter. 68 | } 69 | }).apply("hello", "world"); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/ReturnRecordingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.anyInt; 5 | import static org.mockito.ArgumentMatchers.eq; 6 | import static org.mockito.Mockito.mock; 7 | import static org.mockito.Mockito.times; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import org.junit.Test; 11 | 12 | import hu.advancedweb.scott.helper.InstrumentedObject; 13 | import hu.advancedweb.scott.helper.TestScottRuntime; 14 | import hu.advancedweb.scott.helper.TestScottRuntimeVerifier; 15 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 16 | 17 | public class ReturnRecordingTest { 18 | 19 | Configuration config = new Configuration.Builder() 20 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 21 | .build(); 22 | 23 | @Test 24 | public void recordReturnFromLambda() throws Exception { 25 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 26 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("simpleReturn"); 27 | 28 | // Simple tracking happens in the lambda, and in the containing method as well. 29 | verify(testRuntime, times(2)).trackReturn(anyInt(), any(), any()); 30 | }); 31 | } 32 | 33 | @Test 34 | public void recordReturnPrimitiveTrueFromLambda() throws Exception { 35 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 36 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("primitiveBooleanTrue"); 37 | verify(testRuntime, times(1)).trackReturn(eq(true), anyInt(), any(), any()); 38 | }); 39 | } 40 | 41 | @Test 42 | public void recordReturnPrimitiveFalseFromLambda() throws Exception { 43 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 44 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("primitiveBooleanFalse"); 45 | verify(testRuntime, times(1)).trackReturn(eq(false), anyInt(), any(), any()); 46 | }); 47 | } 48 | 49 | @Test 50 | public void recordReturnPrimitiveIntLambda() throws Exception { 51 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 52 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("primitiveInt"); 53 | verify(testRuntime, times(1)).trackReturn(eq(2147483647), anyInt(), any(), any()); 54 | }); 55 | } 56 | 57 | @Test 58 | public void recordReturnPrimitiveDoubleLambda() throws Exception { 59 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 60 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("primitiveDouble"); 61 | verify(testRuntime, times(1)).trackReturn(eq(1.7976931348623157E308D), anyInt(), any(), any()); 62 | }); 63 | } 64 | 65 | @Test 66 | public void recordReturnPrimitiveLongLambda() throws Exception { 67 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 68 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("primitiveLong"); 69 | verify(testRuntime, times(1)).trackReturn(eq(9223372036854775807L), anyInt(), any(), any()); 70 | }); 71 | } 72 | 73 | @Test 74 | public void recordReturnObjectLambda() throws Exception { 75 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 76 | InstrumentedObject.create("hu.advancedweb.scott.examples.ClassWithLambdas", config).invokeMethod("object"); 77 | verify(testRuntime, times(1)).trackReturn(eq("Hello world"), anyInt(), any(), any()); 78 | }); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /scott-gradle-plugin/src/main/java/hu/advancedweb/scott/ScottPlugin.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import org.gradle.api.Plugin; 4 | import org.gradle.api.Project; 5 | import org.gradle.api.Task; 6 | import org.gradle.api.artifacts.Configuration; 7 | import org.gradle.process.CommandLineArgumentProvider; 8 | import org.gradle.process.JavaForkOptions; 9 | 10 | import java.io.File; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | public class ScottPlugin implements Plugin { 16 | 17 | public static final String DEFAULT_SCOTT_VERSION = "4.0.1"; 18 | public static final String AGENT_CONFIGURATION_NAME = "scottAgent"; 19 | public static final String PLUGIN_EXTENSION_NAME = "scott"; 20 | 21 | @Override 22 | public void apply(Project project) { 23 | 24 | ScottPluginExtension extension = project.getExtensions().create(PLUGIN_EXTENSION_NAME, ScottPluginExtension.class, DEFAULT_SCOTT_VERSION); 25 | Configuration configuration = configureAgentDependencies(project,extension); 26 | 27 | project.afterEvaluate( p -> { 28 | Task test = p.getTasks().getByName("test"); 29 | JavaForkOptions opts = (JavaForkOptions)test; 30 | opts.getJvmArgumentProviders().add(new ScottAgent(configuration,extension)); 31 | }); 32 | } 33 | 34 | private Configuration configureAgentDependencies(Project project, ScottPluginExtension extension) { 35 | Configuration agentConf = project.getConfigurations().create(AGENT_CONFIGURATION_NAME); 36 | agentConf.setVisible(false); 37 | agentConf.setTransitive(true); 38 | agentConf.setDescription("The Scott agent to use detailed failure reports and hassle free assertions for Java tests"); 39 | agentConf.defaultDependencies(dependencies -> 40 | dependencies.add(project.getDependencies().create("hu.advancedweb:scott:" + extension.getToolVersion())) 41 | ); 42 | return agentConf; 43 | } 44 | 45 | static class ScottAgent implements CommandLineArgumentProvider { 46 | private Configuration agentConfig; 47 | private ScottPluginExtension scottPluginExtension; 48 | 49 | private File agentJar; 50 | public ScottAgent(Configuration agentConfig, ScottPluginExtension scottPluginExtension){ 51 | this.agentConfig = agentConfig; 52 | this.scottPluginExtension = scottPluginExtension; 53 | } 54 | 55 | @Override 56 | public Iterable asArguments() { 57 | return Arrays.asList(getArgument()); 58 | } 59 | 60 | private String getArgument() { 61 | return String.format("-javaagent:%s", getJar()) 62 | + createScottArgument("scott.track.method_annotation", scottPluginExtension.getTrackMethodAnnotations()) 63 | + createScottArgument("scott.inject_junit4_rule.method_annotation", scottPluginExtension.getJunit4RuleMethodAnnotations()) 64 | + createScottArgument("scott.inject_junit5_extension.method_annotation", scottPluginExtension.getJunit5RuleMethodAnnotations()); 65 | } 66 | 67 | private String createScottArgument(String name, List arguments) { 68 | if (arguments == null || arguments.isEmpty()) 69 | return ""; 70 | 71 | return String.format(" -D%s=\"%s\"", name, arguments.stream().collect(Collectors.joining( "," ))); 72 | } 73 | 74 | private File getJar() { 75 | if (agentJar == null) { 76 | agentJar = agentConfig 77 | .filter( (file) -> file.getName().startsWith("scott")) 78 | .getSingleFile(); 79 | } 80 | return agentJar; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /scott-examples/junit4/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - JUnit 4 Example project 2 | ============================================= 3 | 4 | This project contains the necessary setup configuration to use Scott with JUnit 4 5 | (see [pom.xml](https://github.com/dodie/scott/blob/master/scott-examples/junit4/pom.xml)) 6 | and a bunch of failing tests to demonstrate the detailed failure messages. 7 | 8 | 9 | Usage 10 | ----- 11 | Run ``` mvn install ``` to see the tests failing. For a Docker-based setup, see the [development guide](https://github.com/dodie/scott/blob/master/docs/development-guide.md). 12 | 13 | 14 | Demo 15 | ---- 16 | The following snippets are from the console output of the example project: 17 | 18 | **Tracking object state:** 19 | ```java 20 | CounterTest.test_1 21 | 9| @Test 22 | 10| public void test_1() { 23 | 11| Counter counter = new Counter(); // counter=Counter [state=0] 24 | 12| 25 | 13| counter.increase(); // counter=Counter [state=1] 26 | 14| counter.increase(); // counter=Counter [state=2] 27 | 15| 28 | 16| int state = counter.get(); // state=2 29 | 17| 30 | 18|* assertEquals(state, 3); // AssertionError: expected:<2> but was:<3> 31 | 19| } 32 | ``` 33 | 34 | 35 | **Reporting the current values of the related fields:** 36 | ```java 37 | ParameterizedTest.testAddition[3] 38 | 37| @Test 39 | 38| public void testAddition() { 40 | | // => this.a=2 41 | | // => this.b=2 42 | | // => this.expectedSum=4 43 | | 44 | 39| int sum = FaultyAdder.add(a, b); // sum=5 45 | 40|* assertThat(sum, equalTo(expectedSum)); // AssertionError: Expected: <4> 46 | | // but: was <5> 47 | 41| } 48 | ``` 49 | 50 | 51 | **Lambda support:** 52 | ```java 53 | LambdaTest.test_with_lambda 54 | 12| @Test 55 | 13| public void test_with_lambda() throws Exception { 56 | 14| Function generatePalindrome = input -> { // generatePalindrome=hu.advancedweb.example.LambdaTest$$Lambda$1/1486371051@402a079c 57 | | // => input="cat" 58 | | 59 | 15| StringBuilder sb = new StringBuilder(); // sb= 60 | 16| sb.append(input); // sb=cat 61 | 17| sb.reverse(); // sb=tac 62 | 18| 63 | 19| String reversed = sb.toString(); // reversed="tac" 64 | 20| 65 | 21| String palindrome = input + reversed; // palindrome="cattac" 66 | 22| return palindrome; 67 | 23| }; 68 | 24| 69 | 25| String word = "cat"; // word="cat" 70 | 26| 71 | 27| String palindromized = generatePalindrome.apply(word); // palindromized="cattac" 72 | 28| 73 | 29|* assertThat(palindromized, equalTo(word + word)); // AssertionError: Expected: "catcat" 74 | | // but: was "cattac" 75 | 30| } 76 | ``` 77 | 78 | 79 | **Oh no, sorting the collection mutates the backing array!** 80 | ```java 81 | ListTest.test_2 82 | 39| @Test 83 | 40| public void test_2() throws Throwable { 84 | 41| Integer[] array = new Integer[] { 1, 4, 2, 3 }; // array=[1, 4, 2, 3] 85 | 42| List list = Arrays.asList(array); // list=[1, 4, 2, 3] 86 | 43| Collections.sort(list); // array=[1, 2, 3, 4] 87 | | // list=[1, 2, 3, 4] 88 | 44| 89 | 45|* assertArrayEquals(array, new Integer[] { 1, 4, 2, 3 }); // ArrayComparisonFailure: arrays first differed at element [1]; expected:<2> but was:<4> 90 | 46| } 91 | ``` 92 | 93 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/runtime/report/ScottReport.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.runtime.report; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.SortedMap; 9 | import java.util.TreeMap; 10 | 11 | /** 12 | * Model object for the detailed failure report. 13 | * 14 | * @author David Csakvari 15 | */ 16 | class ScottReport { 17 | 18 | private final TreeMap sourceForLineNumbers = new TreeMap<>(); 19 | private final Map> initialSnapshots = new TreeMap<>(); 20 | private final Map> snapshotsForLineNumbers = new TreeMap<>(); 21 | private int beginLineNumber; 22 | private int exceptionLineNumber; 23 | private String exceptionMessage; 24 | private String exceptionClassName; 25 | 26 | public void setBeginLine(int beginLine) { 27 | this.beginLineNumber = beginLine; 28 | } 29 | 30 | public void addLine(String source) { 31 | sourceForLineNumbers.put(beginLineNumber, source); 32 | beginLineNumber++; 33 | } 34 | 35 | public void addInitialSnapshot(int lineNumber, String name, String value) { 36 | List variableSnapshots = initialSnapshots.getOrDefault(lineNumber, new ArrayList()); 37 | variableSnapshots.add(new Snapshot(name, value)); 38 | initialSnapshots.put(lineNumber, variableSnapshots); 39 | } 40 | 41 | public void addSnapshot(int lineNumber, String name, String value) { 42 | checkIfSourceFound(lineNumber); 43 | List variableSnapshots = snapshotsForLineNumbers.getOrDefault(lineNumber, new ArrayList()); 44 | variableSnapshots.add(new Snapshot(name, value)); 45 | snapshotsForLineNumbers.put(lineNumber, variableSnapshots); 46 | } 47 | 48 | public void setException(int lineNumber, String exceptionClassName, String exceptionMessage) { 49 | checkIfSourceFound(lineNumber); 50 | this.exceptionLineNumber = lineNumber; 51 | this.exceptionClassName = exceptionClassName; 52 | this.exceptionMessage = exceptionMessage; 53 | } 54 | 55 | public SortedMap getSourceLines() { 56 | return sourceForLineNumbers; 57 | } 58 | 59 | public List getVariableSnapshots(int lineNumber) { 60 | return Collections.unmodifiableList(snapshotsForLineNumbers.getOrDefault(lineNumber, new ArrayList())); 61 | } 62 | 63 | public Map> getVariableMapSnapshot(int lineNumber) { 64 | Map> theMap = new HashMap<>(); 65 | List originals = getVariableSnapshots(lineNumber); 66 | if(originals != null) { 67 | for (Snapshot original : originals) { 68 | String name = original.name; 69 | if (theMap.containsKey(name)) { 70 | theMap.get(name).add(original); 71 | } else { 72 | List snapshots = new ArrayList<>(); 73 | snapshots.add(original); 74 | theMap.put(name, snapshots); 75 | } 76 | } 77 | } 78 | return theMap; 79 | } 80 | 81 | 82 | public List getInitialSnapshots(int lineNumber) { 83 | return Collections.unmodifiableList(initialSnapshots.getOrDefault(lineNumber, new ArrayList())); 84 | } 85 | 86 | public int getBeginLineNumber() { 87 | return beginLineNumber; 88 | } 89 | 90 | public int getExceptionLineNumber() { 91 | return exceptionLineNumber; 92 | } 93 | 94 | public String getExceptionMessage() { 95 | return exceptionMessage; 96 | } 97 | 98 | public String getExceptionClassName() { 99 | return exceptionClassName; 100 | } 101 | 102 | private void checkIfSourceFound(int lineNumber) { 103 | if (!sourceForLineNumbers.containsKey(lineNumber)) { 104 | sourceForLineNumbers.put(lineNumber, "???"); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /scott-maven-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | hu.advancedweb 6 | scott-maven-plugin 7 | 4.0.1 8 | https://github.com/dodie/scott 9 | maven-plugin 10 | 11 | 12 | UTF-8 13 | 14 | 15 | Scott Test Reporter - Maven Plugin 16 | 17 | Scott provides detailed failure messages for tests written 18 | in Java based on their runtime behaviour and source code. 19 | 20 | This plugin eases project configuration for Scott. 21 | 22 | 23 | 24 | 25 | MIT License 26 | http://www.opensource.org/licenses/mit-license.php 27 | 28 | 29 | 30 | 31 | 32 | David Csakvari 33 | dodiehun@gmail.com 34 | advancedweb.hu 35 | https://advancedweb.hu 36 | 37 | 38 | 39 | 40 | https://github.com/dodie/scott/tree/master 41 | scm:git:git://github.com/dodie/scott.git 42 | scm:git:ssh://github.com:dodie/scott.git 43 | 44 | 45 | 46 | 2.2.1 47 | 48 | 49 | 50 | src 51 | 52 | 53 | META-INF 54 | META-INF 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-plugin-plugin 61 | 3.6.1 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-compiler-plugin 66 | 3.8.1 67 | 68 | 1.7 69 | 1.7 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-source-plugin 75 | 3.2.1 76 | 77 | 78 | attach-sources 79 | 80 | jar 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-javadoc-plugin 88 | 3.3.1 89 | 90 | 7 91 | 92 | 93 | 94 | attach-javadocs 95 | 96 | jar 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.apache.maven 107 | maven-plugin-api 108 | ${project.prerequisites.maven} 109 | 110 | 111 | org.apache.maven 112 | maven-project 113 | ${project.prerequisites.maven} 114 | 115 | 116 | ${project.groupId} 117 | scott 118 | ${project.version} 119 | 120 | 121 | org.apache.maven.plugin-tools 122 | maven-plugin-annotations 123 | 3.6.1 124 | provided 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /scott/src/main/java/hu/advancedweb/scott/instrumentation/transformation/param/DiscoveryMethodVisitor.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott.instrumentation.transformation.param; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 9 | import org.objectweb.asm.AnnotationVisitor; 10 | import org.objectweb.asm.Label; 11 | import org.objectweb.asm.MethodVisitor; 12 | import org.objectweb.asm.Opcodes; 13 | import org.objectweb.asm.Type; 14 | 15 | /** 16 | * Determines the instrumentation rules based on method properties. 17 | * 18 | * @author David Csakvari 19 | */ 20 | public class DiscoveryMethodVisitor extends MethodVisitor { 21 | 22 | private InstrumentationActions.Builder instrumentationActions; 23 | private Configuration configuration; 24 | private String methodName; 25 | private String methodDesc; 26 | private String methodSignature; 27 | 28 | private List methodAnnotationFqns = new ArrayList<>(); 29 | private List classAnnotationFqns; 30 | private Set lineNumbers = new HashSet<>(); 31 | 32 | DiscoveryMethodVisitor(MethodVisitor mv, InstrumentationActions.Builder instrumentationActions, 33 | Configuration configuration, String name, String desc, String signature, List classAnnotationFqns) { 34 | super(Opcodes.ASM9, mv); 35 | this.instrumentationActions = instrumentationActions; 36 | this.configuration = configuration; 37 | this.methodName = name; 38 | this.methodDesc = desc; 39 | this.methodSignature = signature; 40 | this.classAnnotationFqns = classAnnotationFqns; 41 | } 42 | 43 | @Override 44 | public void visitLineNumber(int line, Label start) { 45 | lineNumbers.add(line); 46 | super.visitLineNumber(line, start); 47 | } 48 | 49 | @Override 50 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 51 | methodAnnotationFqns.add(Type.getType(descriptor).getClassName()); 52 | return super.visitAnnotation(descriptor, visible); 53 | } 54 | 55 | @Override 56 | public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { 57 | if (isScottInstrumentedTrackMethodInst(opcode, owner, name)) { 58 | instrumentationActions.alreadyInstrumented(true); 59 | } 60 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 61 | } 62 | 63 | private boolean isScottInstrumentedTrackMethodInst(int opcode, String owner, String name) { 64 | return Opcodes.INVOKESTATIC == opcode && 65 | configuration.getTrackerClass().replace('.', '/').equals(owner) && 66 | name.startsWith("track"); 67 | } 68 | 69 | @Override 70 | public void visitEnd() { 71 | boolean isLambda = methodName.startsWith("lambda$"); 72 | if (isLambda) { 73 | if (configuration.isLambdaInstrumentationAllowed(getMethodLoc())) { 74 | instrumentationActions.markLambdaForTracking(methodName, methodDesc, methodSignature); 75 | } 76 | } else if (configuration.isMethodInstrumentationAllowed(methodName, getMethodLoc(), methodAnnotationFqns, classAnnotationFqns)) { 77 | instrumentationActions.markMethodForTracking(methodName, methodDesc, methodSignature); 78 | } 79 | 80 | if (configuration.isJUnit4RuleInjectionRequired(methodAnnotationFqns)) { 81 | instrumentationActions.markClassForJUnit4RuleInjection(); 82 | } 83 | 84 | if (configuration.isJUnit5ExtensionInjectionRequired(methodAnnotationFqns)) { 85 | instrumentationActions.markClassForJUnit5ExtensionInjection(); 86 | } 87 | 88 | super.visitEnd(); 89 | } 90 | 91 | private int getMethodLoc() { 92 | if (lineNumbers.isEmpty()) { 93 | return 0; 94 | } 95 | 96 | int firstLine = Integer.MAX_VALUE; 97 | int lastLine = 0; 98 | 99 | for (int lineNumber: lineNumbers) { 100 | if (lineNumber < firstLine) { 101 | firstLine = lineNumber; 102 | } 103 | if (lastLine < lineNumber) { 104 | lastLine = lineNumber; 105 | } 106 | } 107 | 108 | return lastLine - firstLine + 1; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | ## Configuring the automatic tracking behavior 3 | In case you are not satisfied with the default tracking behavior, the Scott Maven Plugin and Gradle Plugin provides configuration 4 | options: 5 | 6 | | Parameter name | Description | Default value | 7 | | ------------- | ------------- | ------------- | 8 | | trackMethodAnnotations | Collect runtime data from a method if it's marked with at least one of the specified annotations. | org.junit.Test, org.junit.jupiter.api.Test, org.junit.jupiter.api.TestFactory, cucumber.api.java.\* | 9 | | junit4RuleMethodAnnotations | Inject ```ScottReportingRule``` to catch failing tests for JUnit4, if the class has at least one method with at least one of the following annotations. | org.junit.Test | 10 | | junit5RuleMethodAnnotations | Inject ```ScottJUnit5Extension``` to catch failing tests for JUnit5, if the class has at least one method with at least one of the following annotations. | org.junit.jupiter.api.Test, org.junit.jupiter.api.TestFactory | 11 | 12 | 13 | See the following example for the Maven Plugin configuration: 14 | 15 | ```xml 16 | 17 | hu.advancedweb 18 | scott-maven-plugin 19 | ${scott.version} 20 | 21 | 22 | 23 | prepare-agent 24 | 25 | 26 | 27 | 28 | org.junit.Test 29 | org.junit.jupiter.api.Test 30 | org.junit.jupiter.api.TestFactory 31 | cucumber.api 32 | 33 | 34 | org.junit.Test 35 | 36 | 37 | org.junit.jupiter.api.Test 38 | org.junit.jupiter.api.TestFactory 39 | 40 | 41 | 42 | 43 | ``` 44 | With Gradle you can provide these configuration to the plugin by adding an extension object called `scott`: 45 | 46 | ```groovy 47 | scott { 48 | toolVersion ='4.0.1' 49 | trackMethodAnnotations = ['org.junit.Test', 'cucumber.api.java.*'] 50 | } 51 | ``` 52 | 53 | Here, we override the Scott version using the `toolVersion` property and also set the `trackMethodAnnotations` parameter describe above. 54 | 55 | ## Configuring the automatic tracking behavior with command line arguments 56 | The automatic tracking behavior can also be customized by supplying the following configuration parameters: 57 | 58 | | Parameter name | Description | Default value | 59 | | ------------- | ------------- | ------------- | 60 | | scott.track.method_annotation | Collect runtime data from a method if it's marked with at least one of the specified annotations. | "org.junit.Test", "org.junit.jupiter.api.Test", "org.junit.jupiter.api.TestFactory", "cucumber.api.java.\*" | 61 | | scott.inject_junit4_rule.method_annotation | Inject ```ScottReportingRule``` to catch failing tests for JUnit4, if the class has at least one method with at least one of the following annotations. | "org.junit.Test" | 62 | | scott.inject_junit5_extension.method_annotation | Inject ```ScottJUnit5Extension``` to catch failing tests for JUnit5, if the class has at least one method with at least one of the following annotations. | "org.junit.jupiter.api.Test", "org.junit.jupiter.api.TestFactory" | 63 | 64 | Every parameter can contain zero, one or more strings, separated by commas. Each item has to be one of the following: 65 | 66 | - A Fully Qualified Name of an annotation. 67 | - Or an expression that starts with a package name and ends with a ```*```. 68 | 69 | Currently these parameters as to be passed as java arguments. For example: 70 | 71 | ```bash 72 | mvn clean install -Dscott.track.method_annotation="org.junit.Test,cucumber.api.java.*" 73 | ``` 74 | -------------------------------------------------------------------------------- /scott-tests/configuration-based-tests/src/test/java/hu/advancedweb/scott/AnnotationInclusionExclusionTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import org.junit.Test; 4 | 5 | import hu.advancedweb.scott.helper.InstrumentedObject; 6 | import hu.advancedweb.scott.helper.TestScottRuntime; 7 | import hu.advancedweb.scott.helper.TestScottRuntimeVerifier; 8 | import hu.advancedweb.scott.instrumentation.transformation.config.Configuration; 9 | 10 | import static org.mockito.ArgumentMatchers.any; 11 | import static org.mockito.ArgumentMatchers.anyInt; 12 | import static org.mockito.ArgumentMatchers.eq; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.verifyZeroInteractions; 16 | 17 | import java.util.Arrays; 18 | 19 | public class AnnotationInclusionExclusionTest { 20 | 21 | @Test 22 | public void excludeAll() throws Exception { 23 | Configuration config = new Configuration.Builder() 24 | .setExcludeByAnnotation(Arrays.asList("hu.advancedweb.scott.examples.AnnotationA", "hu.advancedweb.scott.examples.AnnotationB")) 25 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 26 | .build(); 27 | 28 | assertNoTrackingMethodInvoked( 29 | "hu.advancedweb.scott.examples.ClassA", 30 | config); 31 | assertNoTrackingMethodInvoked( 32 | "hu.advancedweb.scott.examples.somepackage.ClassB", 33 | config); 34 | assertNoTrackingMethodInvoked( 35 | "hu.advancedweb.scott.examples.otherpackage.ClassC", 36 | config); 37 | 38 | } 39 | 40 | @Test 41 | public void includeAll() throws Exception { 42 | Configuration config = new Configuration.Builder() 43 | .setIncludeByAnnotation(Arrays.asList("hu.advancedweb.scott.examples.AnnotationA", "hu.advancedweb.scott.examples.AnnotationB")) 44 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 45 | .build(); 46 | 47 | assertTracking( 48 | "hu.advancedweb.scott.examples.ClassA", 49 | config); 50 | assertTracking( 51 | "hu.advancedweb.scott.examples.somepackage.ClassB", 52 | config); 53 | assertTracking( 54 | "hu.advancedweb.scott.examples.otherpackage.ClassC", 55 | config); 56 | } 57 | 58 | @Test 59 | public void includeSome() throws Exception { 60 | Configuration config = new Configuration.Builder() 61 | .setIncludeByAnnotation(Arrays.asList("hu.advancedweb.scott.examples.AnnotationA")) 62 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 63 | .build(); 64 | 65 | assertTracking( 66 | "hu.advancedweb.scott.examples.ClassA", 67 | config); 68 | assertTracking( 69 | "hu.advancedweb.scott.examples.somepackage.ClassB", 70 | config); 71 | assertNoTrackingMethodInvoked( 72 | "hu.advancedweb.scott.examples.otherpackage.ClassC", 73 | config); 74 | } 75 | 76 | @Test 77 | public void excludeSome() throws Exception { 78 | Configuration config = new Configuration.Builder() 79 | .setExcludeByAnnotation(Arrays.asList("hu.advancedweb.scott.examples.AnnotationA")) 80 | .setTrackerClass(TestScottRuntime.class.getCanonicalName()) 81 | .build(); 82 | 83 | assertNoTrackingMethodInvoked( 84 | "hu.advancedweb.scott.examples.ClassA", 85 | config); 86 | assertNoTrackingMethodInvoked( 87 | "hu.advancedweb.scott.examples.somepackage.ClassB", 88 | config); 89 | assertTracking( 90 | "hu.advancedweb.scott.examples.otherpackage.ClassC", 91 | config); 92 | } 93 | 94 | private void assertNoTrackingMethodInvoked(String name, Configuration configuration) { 95 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 96 | InstrumentedObject.create(name, configuration).invokeMethod("hello"); 97 | 98 | verifyZeroInteractions(testRuntime); 99 | }); 100 | } 101 | 102 | private void assertTracking(String name, Configuration configuration) { 103 | TestScottRuntime.verify(mock(TestScottRuntimeVerifier.class), testRuntime -> { 104 | InstrumentedObject.create(name, configuration).invokeMethod("hello"); 105 | 106 | verify(testRuntime).trackMethodStart(anyInt(), eq("hello"), any()); 107 | }); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /docs/manual_setup.md: -------------------------------------------------------------------------------- 1 | # Manual setup 2 | 3 | Although Scott has Gradle and Maven plugins to ease integration (check this guide for more information [here](https://github.com/dodie/scott)), it is possible to integrate it manually, to have full control over the instrumentation. 4 | 5 | There are two options: 6 | 7 | 1. Specify the agent manually 8 | 2. Transform the classes after compilation 9 | 10 | 11 | ## 1. Specify the agent manually 12 | 13 | If you can't use the Gradle or Maven Plugin for some reason, you can do the necessary steps manually. 14 | 15 | 1. Get Scott as a dependency. 16 | 2. Extract scott-agent.jar. 17 | 3. Specify the ```-javaagent:/scott-agent.jar``` to your application. 18 | 19 | It can be done in multiple ways. For an example to do exactly this with Maven, see the following example. 20 | 21 | ```xml 22 | 23 | 24 | 25 | ... 26 | 27 | 29 | 30 | org.apache.maven.plugins 31 | maven-dependency-plugin 32 | 2.5.1 33 | 34 | 35 | copy-agent 36 | process-test-classes 37 | 38 | copy 39 | 40 | 41 | 42 | 43 | hu.advancedweb 44 | scott 45 | ${project.build.directory}/agents 46 | scott-agent.jar 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | org.apache.maven.plugins 59 | maven-surefire-plugin 60 | 61 | -javaagent:${project.build.directory}/agents/scott-agent.jar 62 | ${basedir}/target 63 | 64 | 65 | 66 | ... 67 | 68 | 69 | 70 | 71 | 72 | ... 73 | 74 | 75 | 76 | hu.advancedweb 77 | scott 78 | ${scott.version} 79 | test 80 | 81 | 82 | junit 83 | junit 84 | 4.12 85 | test 86 | 87 | 88 | ... 89 | 90 | 91 | ``` 92 | 93 | ## 2. Transform the classes after compilation 94 | 95 | It's possible modify existing class files (or create new class files based on existing class files) to include the 96 | instrumentation. 97 | 98 | 1. Get Scott as a dependency. 99 | 2. Use the [ScottClassFileTransformer](https://github.com/dodie/scott/blob/master/scott/src/main/java/hu/advancedweb/scott/instrumentation/ScottClassFileTransformer.java) to transform class files as part of the build process. When using it, make sure to have all runtime dependencies of the code available on the classpath. For more information, see Issue #79. 100 | 3. Make sure to include Scott as a run-time dependency to your application for the instrumentation to work. 101 | 102 | The following Gradle setup achieves something similar: 103 | 104 | ```groovy 105 | import hu.advancedweb.scott.instrumentation.ScottClassFileTransformer 106 | 107 | buildscript { 108 | dependencies { 109 | classpath "hu.advancedweb:scott:4.0.1" 110 | } 111 | } 112 | 113 | task instrument(type: JavaExec) { 114 | main = 'hu.advancedweb.scott.instrumentation.ScottClassFileTransformer' 115 | args = [sourceSets.main.java.outputDir.absolutePath] 116 | classpath = buildscript.configurations.classpath 117 | classpath += sourceSets.main.runtimeClasspath 118 | } 119 | 120 | compileJava.finalizedBy instrument 121 | ``` 122 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/LambdaRecordingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.TestHelper.wrapped; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import java.util.function.Function; 8 | 9 | import org.junit.Test; 10 | 11 | public class LambdaRecordingTest { 12 | 13 | @Test 14 | public void test_with_lambda() throws Exception { 15 | Function lambda = input -> { 16 | assertThat(TestHelper.getLastRecordedStateForVariable("input"), equalTo(wrapped(input))); 17 | String inner = "inner"; 18 | assertThat(TestHelper.getLastRecordedStateForVariable("inner"), equalTo(wrapped(inner))); 19 | return inner; 20 | }; 21 | assertThat(TestHelper.getLastRecordedStateForVariable("lambda"), equalTo(lambda.toString())); 22 | 23 | String outer = "outer"; 24 | assertThat(TestHelper.getLastRecordedStateForVariable("outer"), equalTo(wrapped(outer))); 25 | String result = lambda.apply(outer); 26 | assertThat(TestHelper.getLastRecordedStateForVariable("result"), equalTo(wrapped(result))); 27 | } 28 | 29 | @Test 30 | public void lambda_with_multiple_parameters() throws Exception { 31 | Function2 lambda = (a, b) -> { 32 | assertThat(TestHelper.getLastRecordedStateForVariable("a"), equalTo(wrapped(a))); 33 | assertThat(TestHelper.getLastRecordedStateForVariable("b"), equalTo(wrapped(b))); 34 | return a + b; 35 | }; 36 | String result = lambda.apply("1", "2"); 37 | assertThat(TestHelper.getLastRecordedStateForVariable("result"), equalTo(wrapped(result))); 38 | } 39 | 40 | @FunctionalInterface 41 | interface Function2 { 42 | public R apply (A a, B b); 43 | } 44 | 45 | @Test 46 | public void lambda_with_single_expression() throws Exception { 47 | Function lambda = a -> a + a; 48 | String result = lambda.apply("1"); 49 | assertThat(TestHelper.getLastRecordedStateForVariable("result"), equalTo(wrapped(result))); 50 | } 51 | 52 | @Test 53 | public void lambda_in_lambda() throws Exception { 54 | Function lambda = input -> { 55 | assertThat(TestHelper.getLastRecordedStateForVariable("input"), equalTo(wrapped(input))); 56 | String inner = "inner" + input; 57 | assertThat(TestHelper.getLastRecordedStateForVariable("inner"), equalTo(wrapped(inner))); 58 | 59 | Function lambdaInner = inputInner -> { 60 | assertThat(TestHelper.getLastRecordedStateForVariable("inputInner"), equalTo(wrapped(inputInner))); 61 | String innerInner = "inner" + inputInner + input; 62 | assertThat(TestHelper.getLastRecordedStateForVariable("innerInner"), equalTo(wrapped(innerInner))); 63 | return innerInner; 64 | }; 65 | 66 | return lambdaInner.apply(inner); 67 | }; 68 | assertThat(TestHelper.getLastRecordedStateForVariable("lambda"), equalTo(lambda.toString())); 69 | 70 | String outer = "outer"; 71 | assertThat(TestHelper.getLastRecordedStateForVariable("outer"), equalTo(wrapped(outer))); 72 | String result = lambda.apply(outer); 73 | assertThat(TestHelper.getLastRecordedStateForVariable("result"), equalTo(wrapped(result))); 74 | } 75 | 76 | @Test 77 | public void static_method_reference() throws Exception { 78 | Function fun = LambdaRecordingTest::myStaticMethod; 79 | String result = fun.apply("1"); 80 | assertThat(TestHelper.getLastRecordedStateForVariable("result"), equalTo(wrapped(result))); 81 | } 82 | 83 | static String myStaticMethod(String s) { 84 | return s + s; 85 | } 86 | 87 | @Test 88 | public void method_reference() throws Exception { 89 | Function fun = this::myMethod; 90 | String result = fun.apply("1"); 91 | assertThat(TestHelper.getLastRecordedStateForVariable("result"), equalTo(wrapped(result))); 92 | } 93 | 94 | String myMethod(String s) { 95 | return s + s; 96 | } 97 | 98 | @Test 99 | public void constructor_reference() throws Exception { 100 | Function constructor = String::new; 101 | String newString = constructor.apply("1"); 102 | assertThat(TestHelper.getLastRecordedStateForVariable("newString"), equalTo(wrapped(newString))); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/ExceptionTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.TestHelper.wrapped; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import org.junit.Test; 8 | 9 | public class ExceptionTest { 10 | 11 | @SuppressWarnings("null") 12 | @Test 13 | public void recordExceptions() throws Exception { 14 | String o = null; 15 | 16 | try { 17 | o.length(); 18 | } catch (Exception e) { 19 | o = "fallback"; 20 | assertThat(TestHelper.getLastRecordedStateForVariable("e"), equalTo(e.toString())); 21 | assertThat(TestHelper.getLastRecordedStateForVariable("o"), equalTo(wrapped(o))); 22 | } 23 | } 24 | 25 | @SuppressWarnings("null") 26 | @Test 27 | public void recordExceptionsWithVariablesInTheTryScope() { 28 | String o = null; 29 | 30 | try { 31 | String inner = "inner"; 32 | assertThat(TestHelper.getLastRecordedStateForVariable("inner"), equalTo(wrapped(inner))); 33 | o.length(); 34 | } catch (Exception e) { 35 | o = "fallback"; 36 | assertThat(TestHelper.getLastRecordedStateForVariable("e"), equalTo(e.toString())); 37 | assertThat(TestHelper.getLastRecordedStateForVariable("o"), equalTo(wrapped(o))); 38 | } 39 | } 40 | 41 | @SuppressWarnings("null") 42 | @Test 43 | public void moreVariablesInTryBlockThanInCatchBlock() { 44 | String o = null; 45 | try { 46 | String inner = "inner"; 47 | assertThat(TestHelper.getLastRecordedStateForVariable("inner"), equalTo(wrapped(inner))); 48 | String inner2 = "inner2"; 49 | assertThat(TestHelper.getLastRecordedStateForVariable("inner2"), equalTo(wrapped(inner2))); 50 | o.length(); 51 | } catch (Exception e) { 52 | o = "fallback"; 53 | assertThat(TestHelper.getLastRecordedStateForVariable("e"), equalTo(e.toString())); 54 | assertThat(TestHelper.getLastRecordedStateForVariable("o"), equalTo(wrapped(o))); 55 | } 56 | } 57 | 58 | @SuppressWarnings("null") 59 | @Test 60 | public void nestedTryCatchBlocks() { 61 | String o = null; 62 | try { 63 | String inner = "inner"; 64 | assertThat(TestHelper.getLastRecordedStateForVariable("inner"), equalTo(wrapped(inner))); 65 | String inner2 = "inner2"; 66 | assertThat(TestHelper.getLastRecordedStateForVariable("inner2"), equalTo(wrapped(inner2))); 67 | o.length(); 68 | String o_2 = null; 69 | try { 70 | String inner_2 = "inner"; 71 | assertThat(TestHelper.getLastRecordedStateForVariable("inner_2"), equalTo(wrapped(inner_2))); 72 | String inner2_2 = "inner2"; 73 | assertThat(TestHelper.getLastRecordedStateForVariable("inner2_2"), equalTo(wrapped(inner2_2))); 74 | o.length(); 75 | } catch (Exception e_2) { 76 | o_2 = "fallback"; 77 | assertThat(TestHelper.getLastRecordedStateForVariable("e_2"), equalTo(e_2.toString())); 78 | assertThat(TestHelper.getLastRecordedStateForVariable("o_2"), equalTo(wrapped(o_2))); 79 | } 80 | 81 | } catch (Exception e) { 82 | o = "fallback"; 83 | assertThat(TestHelper.getLastRecordedStateForVariable("e"), equalTo(e.toString())); 84 | assertThat(TestHelper.getLastRecordedStateForVariable("o"), equalTo(wrapped(o))); 85 | } 86 | } 87 | 88 | @SuppressWarnings("null") 89 | @Test 90 | public void nestedTryCatchBlocks_2() { 91 | String o = null; 92 | try { 93 | String inner = "inner"; 94 | assertThat(TestHelper.getLastRecordedStateForVariable("inner"), equalTo(wrapped(inner))); 95 | String inner2 = "inner2"; 96 | assertThat(TestHelper.getLastRecordedStateForVariable("inner2"), equalTo(wrapped(inner2))); 97 | o.length(); 98 | } catch (Exception e) { 99 | o = "fallback"; 100 | assertThat(TestHelper.getLastRecordedStateForVariable("e"), equalTo(e.toString())); 101 | assertThat(TestHelper.getLastRecordedStateForVariable("o"), equalTo(wrapped(o))); 102 | 103 | String o_2 = null; 104 | try { 105 | String inner_2 = "inner"; 106 | assertThat(TestHelper.getLastRecordedStateForVariable("inner_2"), equalTo(wrapped(inner_2))); 107 | String inner2_2 = "inner2"; 108 | assertThat(TestHelper.getLastRecordedStateForVariable("inner2_2"), equalTo(wrapped(inner2_2))); 109 | o.length(); 110 | } catch (Exception e_2) { 111 | o_2 = "fallback"; 112 | assertThat(TestHelper.getLastRecordedStateForVariable("e_2"), equalTo(e_2.toString())); 113 | assertThat(TestHelper.getLastRecordedStateForVariable("o_2"), equalTo(wrapped(o_2))); 114 | } 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /scott-tests/junit4-tests/src/test/java/hu/advancedweb/scott/StaticFieldRecordingTest.java: -------------------------------------------------------------------------------- 1 | package hu.advancedweb.scott; 2 | 3 | import static hu.advancedweb.scott.TestHelper.wrapped; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import org.junit.Ignore; 8 | import org.junit.Test; 9 | 10 | public class StaticFieldRecordingTest extends StaticFieldRecordingTestHelperSuperClass implements StaticFieldRecordingTestHelperInterface { 11 | 12 | static byte B = 0; 13 | static short S = 0; 14 | static int I = 0; 15 | static long L = 0L; 16 | static float F = 0.0F; 17 | static double D = 0.0D; 18 | static boolean BOOL = false; 19 | static char C = 'i'; 20 | static String OBJECT = "initial"; 21 | 22 | 23 | @Test 24 | public void recordInteger() throws Exception { 25 | I = 5; 26 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.I"), equalTo(Integer.toString(I))); 27 | } 28 | 29 | @Test 30 | public void recordShort() throws Exception { 31 | S = 500; 32 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.S"), equalTo(Short.toString(S))); 33 | } 34 | 35 | @Test 36 | public void recordLong() throws Exception { 37 | L = 1000L; 38 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.L"), equalTo(Long.toString(L))); 39 | } 40 | 41 | @Test 42 | public void recordDouble() throws Exception { 43 | D = 5.5D; 44 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.D"), equalTo(Double.toString(D))); 45 | } 46 | 47 | @Test 48 | public void recordFloat() throws Exception { 49 | F = 5.5F; 50 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.F"), equalTo(Float.toString(F))); 51 | } 52 | 53 | @Test 54 | public void recordBoolean() throws Exception { 55 | BOOL = true; 56 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.BOOL"), equalTo(Boolean.toString(BOOL))); 57 | } 58 | 59 | @Test 60 | public void recordString() throws Exception { 61 | OBJECT = "Hello World!"; 62 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.OBJECT"), equalTo(wrapped(OBJECT))); 63 | } 64 | 65 | @Test 66 | public void recordForeignStaticField() throws Exception { 67 | String s = StaticFieldRecordingTestHelper.SOME_VALUE_TO_READ; 68 | assertThat(s, equalTo(StaticFieldRecordingTestHelper.SOME_VALUE_TO_READ)); 69 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTestHelper.SOME_VALUE_TO_READ"), equalTo(wrapped("42"))); 70 | } 71 | 72 | @Test 73 | public void recordForeignStaticFieldModification() throws Exception { 74 | /* 75 | * Note that Scott can't track constants (public static final), because they get inlined at compile time. 76 | * The observer static fields in the test are not final. 77 | */ 78 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTestHelper.SOME_VALUE_TO_WRITE"), equalTo(wrapped("before_write"))); 79 | StaticFieldRecordingTestHelper.SOME_VALUE_TO_WRITE = "after_write"; 80 | assertThat(StaticFieldRecordingTestHelper.SOME_VALUE_TO_WRITE, equalTo("after_write")); 81 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTestHelper.SOME_VALUE_TO_WRITE"), equalTo(wrapped("after_write"))); 82 | } 83 | 84 | @Ignore 85 | @Test 86 | public void recordInterfaceStaticField() throws Exception { 87 | /* 88 | * This test is intentionally ignored, because Scott can't track constants (public static final). 89 | * The static fields from interfaces are final by design. 90 | */ 91 | String s = SOME_VALUE_FROM_INTERFACE; 92 | assertThat(s, equalTo(SOME_VALUE_FROM_INTERFACE)); 93 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTestHelperInterface.SOME_VALUE_FROM_INTERFACE"), equalTo("42")); 94 | } 95 | 96 | @Test 97 | public void recordSuperClassStaticField() throws Exception { 98 | String s = SOME_VALUE_FROM_SUPER_TO_READ; 99 | assertThat(s, equalTo(SOME_VALUE_FROM_SUPER_TO_READ)); 100 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.SOME_VALUE_FROM_SUPER_TO_READ"), equalTo(wrapped("super"))); 101 | } 102 | 103 | @Test 104 | public void recordSuperClassStaticFieldModification() throws Exception { 105 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.SOME_VALUE_FROM_SUPER_TO_WRITE"), equalTo(wrapped("before_write"))); 106 | SOME_VALUE_FROM_SUPER_TO_WRITE = "after_write"; 107 | assertThat(SOME_VALUE_FROM_SUPER_TO_WRITE, equalTo("after_write")); 108 | assertThat(TestHelper.getLastRecordedStateForField("StaticFieldRecordingTest.SOME_VALUE_FROM_SUPER_TO_WRITE"), equalTo(wrapped("after_write"))); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /scott-examples/junit5/readme.md: -------------------------------------------------------------------------------- 1 | Scott Test Reporter - JUnit 5 Example project 2 | =========================================== 3 | 4 | This project contains the necessary setup configuration to use Scott with JUnit 5, 5 | (see [pom.xml](https://github.com/dodie/scott/blob/master/scott-examples/junit5/pom.xml)) 6 | for details. This example works with Java 11. 7 | 8 | Check out the [JUnit 4 example project](https://github.com/dodie/scott/blob/master/scott-examples/junit4) 9 | as well for another bunch of failing tests that demonstrates the detailed failure messages. 10 | 11 | 12 | Usage 13 | ----- 14 | Run ``` mvn install ``` to see the tests failing. For a Docker-based setup, see the [development guide](https://github.com/dodie/scott/blob/master/docs/development-guide.md). 15 | 16 | 17 | Demo 18 | ---- 19 | The following snippets are from the console output of the example project: 20 | 21 | **Asserting exceptions:** 22 | ```java 23 | 48| @Test 24 | 49| @DisplayName("😱") 25 | 50| void exceptionTesting() { 26 | 51|* assertThrows(NullPointerException.class, () -> { // AssertionFailedError: Expected java.lang.NullPointerException to be thrown, but nothing was thrown. 27 | 52| List set = new ArrayList<>(); // set=[] 28 | 53| set.add("I"); // set=[I] 29 | 54| set.add("will"); // set=[I, will] 30 | 55| set.add("not"); // set=[I, will, not] 31 | 56| set.add("explode!"); // set=[I, will, not, explode!] 32 | 57| assertTrue(set.size() > 1); 33 | 58| }); 34 | 59| } 35 | ``` 36 | 37 | **Grouped assertions:** 38 | ```java 39 | 34| @Test 40 | 35| @DisplayName("This is a test with multiple assertions.") 41 | 36| public void testWithGroupedAssertions() { 42 | 37| Integer[] myArray = new Integer[] { 1, 4, 2, 4 }; // myArray=[1, 4, 2, 4] 43 | 38| List myList = Arrays.asList(myArray); // myList=[1, 4, 2, 4] 44 | 39| 45 | 40| Set mySet = new HashSet<>(myList); // mySet=[1, 2, 4] 46 | 41| mySet.remove(4); // mySet=[1, 2] 47 | 42| 48 | 43|* assertAll("mySet is too small", // MultipleFailuresError: mySet is too small (2 failures) 49 | | // It does not contain a whole lot of numbers! 50 | | // It does not even contain 4! 51 | | // => mySet=[1, 2] 52 | | // => mySet=[1, 2] 53 | | 54 | 44| () -> assertTrue(mySet.size() > 2, "It does not contain a whole lot of numbers!"), 55 | 45| () -> assertTrue(mySet.contains(4), "It does not even contain 4!")); 56 | 46| } 57 | ``` 58 | 59 | **Nested classes:** 60 | ```java 61 | NestedClassTest.test 62 | 19| @Test 63 | 20| void test() { 64 | | // => this.value=1 65 | | 66 | 21| String dot = "."; // dot="." 67 | 22| value += dot; // this.value="1." 68 | 23|* assertEquals("1", value); // AssertionFailedError: expected: <1> but was: <1.> 69 | 24| } 70 | 71 | NestedClassTest$NestedClass.test 72 | 36| @Test 73 | 37| void test() { 74 | | // => this.nestedValue=a 75 | | // => (in enclosing NestedClassTest) value=12 76 | | 77 | 38| String dot = "."; // dot="." 78 | 39| nestedValue += dot; // this.nestedValue="a." 79 | 40| value += dot + nestedValue; // (in enclosing NestedClassTest) value="12.a." 80 | 41| 81 | 42|* assertEquals("12.a", value); // AssertionFailedError: expected: <12.a> but was: <12.a.> 82 | 43| } 83 | ``` 84 | 85 | **Exceeding timeout:** 86 | ```java 87 | 61| @Test 88 | 62| void timeoutExceedingTest() { 89 | 63|* assertTimeout(ofMillis(2), () -> { // AssertionFailedError: execution exceeded timeout of 2 ms by 9998 ms 90 | 64| String calculate = "slow"; // calculate="slow" 91 | 65| calculate += "operation"; // calculate="slowoperation" 92 | 66| Thread.sleep(10000L); 93 | 67| }); 94 | 68| } 95 | ``` 96 | 97 | **Not exceeding timeout:** 98 | ```java 99 | 71| @Test 100 | 72| void timeoutNotExceededWithResult() { 101 | 73| String actualResult = assertTimeout(ofMinutes(2), () -> { // actualResult="result" 102 | 74| return "result"; 103 | 75| }); 104 | 76|* assertEquals("no result", actualResult); // AssertionFailedError: expected: but was: 105 | 77| } 106 | ``` 107 | 108 | **Simple test with message supplier:** 109 | ```java 110 | 23| @Test 111 | 24| public void testWithMessageSupplier() { 112 | 25| String first = "Hello"; // first="Hello" 113 | 26| String last = "World"; // last="World" 114 | 27| 115 | 28| String concatenated = first + " " + last; // concatenated="Hello World" 116 | 29| 117 | 30|* assertEquals("Goodbye World", concatenated, // AssertionFailedError: Incorrect message. ==> expected: but was: 118 | 31| () -> "Incorrect message."); 119 | 32| } 120 | ``` 121 | --------------------------------------------------------------------------------