├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASE.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logcapture-core ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── logcapture │ │ ├── LogCapture.java │ │ ├── assertion │ │ ├── ExpectedLoggedException.java │ │ ├── ExpectedLoggingMessage.java │ │ └── VerificationException.java │ │ ├── logback │ │ └── StubAppender.java │ │ └── matcher │ │ ├── ExpectedExceptionMatcher.java │ │ ├── TypedAnythingMatcher.java │ │ └── exception │ │ ├── ExceptionCauseMatcher.java │ │ └── ExceptionCauseMessageMatcher.java │ └── test │ └── java │ └── org │ └── logcapture │ ├── LogCaptureShould.java │ ├── assertion │ ├── ExpectedLoggedExceptionShould.java │ ├── ExpectedLoggingMessageShould.java │ └── VerificationExceptionShould.java │ └── matcher │ ├── ExpectedExceptionMatcherShould.java │ └── exception │ ├── ExceptionCauseMatcherShould.java │ └── ExceptionCauseMessageMatcherShould.java ├── logcapture-example ├── build.gradle └── src │ └── test │ └── java │ └── org │ └── logcapture │ └── example │ └── ExampleShould.java ├── logcapture-junit4 ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── logcapture │ │ └── junit4 │ │ └── LogCaptureRule.java │ └── test │ └── java │ └── org │ └── logcapture │ └── junit4 │ ├── LogCaptureClassRuleShould.java │ └── LogCaptureRuleShould.java ├── logcapture-junit5 ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── logcapture │ │ └── junit5 │ │ └── LogCaptureExtension.java │ └── test │ └── java │ └── org │ └── logcapture │ └── junit5 │ └── LogCaptureRegisterExtensionShould.java ├── logcapture-kotest ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── logcapture │ │ └── kotest │ │ └── LogCaptureListener.kt │ └── test │ └── kotlin │ └── org │ └── logcapture │ └── kotest │ ├── LogCaptureListenerSpec.kt │ └── LogCaptureListenerWithCustomLoggerNameSpec.kt ├── logcapture-spock2 ├── build.gradle └── src │ ├── main │ └── groovy │ │ └── org │ │ └── logcapture │ │ └── spock2 │ │ ├── LogCaptureSpec.groovy │ │ └── LogCaptureTrait.groovy │ └── test │ └── groovy │ └── org │ └── logcapture │ └── spock2 │ ├── LogCaptureSpecShould.groovy │ └── LogCaptureTraitShould.groovy └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | insert_final_newline = true 11 | indent_style = space 12 | 13 | [*.{java,groovy,kt,gradle}] 14 | indent_size = 2 15 | continuation_indent_size = 4 16 | wildcard_import_limit = 9999 17 | curly_bracket_next_line = false 18 | 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gradle" 7 | directory: "/" 8 | schedule: 9 | interval: daily 10 | open-pull-requests-limit: 8 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | time: "09:00" 16 | timezone: Europe/Madrid 17 | open-pull-requests-limit: 8 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Pipeline 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | java: [11, 21] 10 | name: Java ${{ matrix.java }} 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Set up JDK ${{ matrix.java }} 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: ${{ matrix.java }} 18 | distribution: adopt-hotspot 19 | - name: Setup Gradle 20 | uses: gradle/actions/setup-gradle@v4 21 | with: 22 | dependency-graph: generate-and-submit 23 | add-job-summary-as-pr-comment: always 24 | - name: Build with Gradle 25 | run: ./gradlew check jacocoTestReport --scan 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | .gradle 5 | 6 | build 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 Javier Salinas 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jsalinaspolo/logcapture.svg?branch=master)](https://travis-ci.org/jsalinaspolo/logcapture) 2 | [![Sonatype Nexus](https://img.shields.io/nexus/r/org.logcapture/logcapture-core?server=https%3A%2F%2Fs01.oss.sonatype.org)](https://repo1.maven.org/maven2/org/logcapture/) 3 | [![codecov](https://codecov.io/gh/jsalinaspolo/logcapture/branch/master/graph/badge.svg)](https://codecov.io/gh/jsalinaspolo/logcapture) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/jsalinaspolo/logcapture/badge.svg?targetFile=build.gradle)](https://snyk.io/test/github/jsalinaspolo/logcapture?targetFile=build.gradle) 5 | 6 | # LogCapture 7 | 8 | LogCapture is a testing library for asserting logging messages. 9 | 10 | > :warning: Latest release with Java 1.8 and/or spock 1.0 support is `1.3.4` 11 | ## How it works 12 | 13 | 14 | Using JUnit Rule: 15 | 16 | ```java 17 | @Rule 18 | public LogCaptureRule logCaptureRule = new LogCaptureRule(); 19 | 20 | @Test 21 | public void verify_logs_using_rule() { 22 | log.info("a message"); 23 | 24 | logCaptureRule.logged(aLog().info().withMessage("a message")); 25 | } 26 | ``` 27 | 28 | Using JUnit 5 Extension: 29 | 30 | ```java 31 | @RegisterExtension 32 | public LogCaptureExtension logCaptureExtension = new LogCaptureExtension(); 33 | 34 | @Test 35 | public void verify_logs_using_extension() { 36 | log.info("a message"); 37 | 38 | logCaptureExtension.logged(aLog().info().withMessage("a message")); 39 | } 40 | ``` 41 | 42 | Using Spock: 43 | 44 | ```groovy 45 | class LogCaptureSpecShould extends LogCaptureSpec { 46 | 47 | @Shared log = LoggerFactory.getLogger(getClass()) 48 | 49 | def "verify log message"() { 50 | expect: 51 | log.info("a message"); 52 | 53 | logged(aLog().info().withMessage("a message")) 54 | } 55 | } 56 | ``` 57 | 58 | Using Kotest: 59 | 60 | ```kotlin 61 | class LogCaptureListenerSpec : StringSpec({ 62 | val logCaptureListener = LogCaptureListener() 63 | listener(logCaptureListener) // Add LogCaptureListener 64 | 65 | val log: Logger = LoggerFactory.getLogger(LogCaptureListenerSpec::class.java) 66 | 67 | "verify log messages" { 68 | log.info("a message") 69 | 70 | logCaptureListener.logged(aLog().info().withMessage("a message")) 71 | } 72 | }) 73 | ``` 74 | 75 | More example how to use the library at [ExampleShould.java](https://github.com/jsalinaspolo/logcapture/blob/main/logcapture-example/src/test/java/org/logcapture/example/ExampleShould.java) 76 | 77 | ## Binaries 78 | 79 | Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.logcapture). 80 | 81 | Gradle 82 | 83 | ``` 84 | testImplementation 'org.logcapture:logcapture-core:x.y.z' 85 | ``` 86 | 87 | add one of the test library dependency 88 | 89 | ``` 90 | testImplementation 'org.logcapture:logcapture-junit4:x.y.z' 91 | testImplementation 'org.logcapture:logcapture-junit5:x.y.z' 92 | testImplementation 'org.logcapture:logcapture-spock:x.y.z' 93 | testImplementation 'org.logcapture:logcapture-kotest:x.y.z' 94 | ``` 95 | 96 | 97 | Maven: 98 | 99 | ```xml 100 | 101 | org.logcapture 102 | logcapture-core 103 | x.y.z 104 | 105 | ``` 106 | 107 | 108 | ## Why LogCapture? 109 | 110 | Logging should be a **first class citizen** in every system that aims to be easily diagnosed and maintained. Logging/testing first could help 111 | you to drive production code. At the same time it is easy to log object references and objects that includes private information like passwords or tokens 112 | and not realising until we actually read production logs. 113 | 114 | We should test how robust are our non-functional capabilities, and not only our functional features. Being able to diagnose, 115 | and ultimately fix, issues is a non-functional dimension that should be subject to the same standards as performance, reliability or security. 116 | 117 | Logging first development could give you the following benefits: 118 | 119 | * Help you to come up with some useful logging that makes sense in context, that exposes enough, and just enough, semantic 120 | information and that does not leak secure information. 121 | * Help you to understand beforehand what are the high level technical details that your design will implement. 122 | * Provide insights to security, support or operations engineers that could have different needs and drivers that application developers. 123 | * Help you to come up with rules for your logging monitoring system. 124 | 125 | ## License 126 | 127 | This project is licensed under [MIT license](http://opensource.org/licenses/MIT). 128 | 129 | ## Contributing 130 | 131 | Github is for social coding: if you want to write code, I encourage contributions through pull requests from forks of this repository. 132 | Create Github tickets for bugs and new features and comment on the ones that you are interested in. 133 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## How to release 2 | 3 | Use username/password of sonatype https://s01.oss.sonatype.org/ 4 | 5 | ```shell 6 | MAVEN_USERNAME={} MAVEN_PASSWORD={} ./gradlew build jar publish 7 | ``` 8 | 9 | ## Generate gpg key 10 | 11 | Generate gpg key with name and email 12 | 13 | ```shell 14 | gpg --gen-key 15 | ``` 16 | 17 | Export keyring 18 | 19 | ```shell 20 | gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg 21 | ``` 22 | 23 | ## Gradle properties 24 | 25 | Gradle properties should looks something like 26 | 27 | ```shell 28 | $ cat ~/.gradle/gradle.properties 29 | 30 | signing.keyId=5DBFACD8 31 | signing.password={passphrase} 32 | signing.secretKeyRingFile=//Users/jsalinas/.gnupg/secring.gpg 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'org.logcapture' 2 | version '1.3.6' 3 | description = 'A testing library for assert logging messages.' 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | subprojects { 10 | apply plugin: 'idea' 11 | apply plugin: 'signing' 12 | apply plugin: 'maven-publish' 13 | apply plugin: 'java-library' 14 | apply plugin: "jacoco" 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | test { 21 | testLogging { 22 | events "passed", "skipped", "failed" 23 | } 24 | } 25 | 26 | java { 27 | toolchain { 28 | languageVersion = JavaLanguageVersion.of(11) 29 | } 30 | 31 | withJavadocJar() 32 | withSourcesJar() 33 | } 34 | 35 | def pomConfig = { 36 | name project.name 37 | description 'A testing library for assert logging messages.' 38 | url "http://www.jspcore.com/" 39 | 40 | licenses { 41 | license { 42 | name "MIT License" 43 | url "https://opensource.org/licenses/MIT" 44 | distribution "repo" 45 | } 46 | } 47 | scm { 48 | connection "scm:https://jsalinaspolo@github.com/jsalinaspolo/logcapture.git" 49 | developerConnection "scm:git@github.com:jsalinaspolo/logcapture.git" 50 | url "https://github.com/jsalinaspolo/logcapture" 51 | } 52 | developers { 53 | developer { 54 | id "jsalinaspolo" 55 | name "Team JSPCore" 56 | } 57 | } 58 | } 59 | 60 | publishing { 61 | repositories { 62 | maven { 63 | name = "OSSRH" 64 | url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 65 | credentials { 66 | username = System.getenv("MAVEN_USERNAME") 67 | password = System.getenv("MAVEN_PASSWORD") 68 | } 69 | } 70 | } 71 | publications { 72 | MyPublication(MavenPublication) { 73 | from components.java 74 | groupId 'org.logcapture' 75 | artifactId project.name 76 | version "1.3.6" 77 | 78 | pom.withXml { 79 | def root = asNode() 80 | root.appendNode('description', 'A testing library for assert logging messages.') 81 | root.children().last() + pomConfig 82 | } 83 | } 84 | } 85 | } 86 | 87 | signing { 88 | sign publishing.publications.MyPublication 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsalinaspolo/logcapture/e7b1d737644c057efb2ee0280c42733e481329f5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /logcapture-core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | dependencies { 6 | api "org.hamcrest:hamcrest-library:3.0" 7 | api "ch.qos.logback:logback-classic:1.5.18" 8 | api "org.slf4j:slf4j-api:2.0.17" 9 | 10 | testImplementation "junit:junit:4.13.2" 11 | testImplementation "org.assertj:assertj-core:3.27.3" 12 | } 13 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/LogCapture.java: -------------------------------------------------------------------------------- 1 | package org.logcapture; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import org.hamcrest.Matcher; 5 | import org.logcapture.assertion.VerificationException; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public class LogCapture { 12 | 13 | private final List events; 14 | 15 | public LogCapture(List events) { 16 | this.events = events; 17 | } 18 | 19 | public LogCapture logged(Matcher> expectedLoggingMessage) { 20 | if (expectedLoggingMessage.matches(events)) { 21 | return this; 22 | } 23 | 24 | throw VerificationException.forUnmatchedLog(expectedLoggingMessage, events); 25 | } 26 | 27 | public LogCapture logged(Matcher> expectedLoggingMessage, Integer times) { 28 | logged(expectedLoggingMessage); 29 | List matchingEvents = events.stream() 30 | .filter(actual -> expectedLoggingMessage.matches(Collections.singletonList(actual))) 31 | .collect(Collectors.toList()); 32 | if (matchingEvents.size() != times) { 33 | throw VerificationException.forUnmatchedTimesLog(expectedLoggingMessage, events, times, events.size()); 34 | } 35 | return this; 36 | } 37 | 38 | public List getEvents() { 39 | return Collections.unmodifiableList(events); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/assertion/ExpectedLoggedException.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.assertion; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import org.logcapture.matcher.ExpectedExceptionMatcher; 5 | import org.logcapture.matcher.TypedAnythingMatcher; 6 | import org.hamcrest.Matcher; 7 | 8 | public class ExpectedLoggedException { 9 | private Matcher expectedMessageMatcher = new TypedAnythingMatcher<>(); 10 | private Matcher expectedException = new TypedAnythingMatcher<>(); 11 | 12 | private ExpectedLoggedException() { 13 | } 14 | 15 | public static ExpectedLoggedException logException() { 16 | return new ExpectedLoggedException(); 17 | } 18 | 19 | public ExpectedLoggedException withException(Matcher exceptionMatcher) { 20 | this.expectedException = new ExpectedExceptionMatcher(exceptionMatcher); 21 | return this; 22 | } 23 | 24 | public boolean matches(ILoggingEvent event) { 25 | if (event.getThrowableProxy() == null) { 26 | return false; 27 | } 28 | 29 | return expectedMessageMatcher.matches(event.getThrowableProxy().getMessage()) && 30 | expectedException.matches(event.getThrowableProxy()); 31 | } 32 | 33 | public ExpectedLoggedException withMessage(Matcher expectedMessageMatcher) { 34 | this.expectedMessageMatcher = expectedMessageMatcher; 35 | return this; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "ExpectedLoggedException{" + 41 | "expectedMessageMatcher=" + expectedMessageMatcher + 42 | ", expectedException=" + expectedException + 43 | '}'; 44 | } 45 | 46 | public static final ExpectedLoggedException ANYTHING = new ExpectedLoggedException() { 47 | public boolean matches(ILoggingEvent event) { 48 | return true; 49 | } 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/assertion/ExpectedLoggingMessage.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.assertion; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import org.logcapture.matcher.TypedAnythingMatcher; 6 | import org.hamcrest.Description; 7 | import org.hamcrest.Matcher; 8 | import org.hamcrest.TypeSafeMatcher; 9 | import org.slf4j.Marker; 10 | import org.slf4j.MarkerFactory; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.Collection; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.function.Predicate; 19 | import java.util.stream.Collectors; 20 | 21 | import static ch.qos.logback.classic.Level.DEBUG; 22 | import static ch.qos.logback.classic.Level.ERROR; 23 | import static ch.qos.logback.classic.Level.INFO; 24 | import static ch.qos.logback.classic.Level.WARN; 25 | import static java.util.Collections.singleton; 26 | import static org.hamcrest.Matchers.equalTo; 27 | 28 | public class ExpectedLoggingMessage extends TypeSafeMatcher> { 29 | 30 | private Matcher logLevelMatcher = new TypedAnythingMatcher<>(); 31 | private Matcher markerMatcher = new TypedAnythingMatcher<>(); 32 | private List> expectedMessageMatcher = new ArrayList<>(); 33 | private Matcher expectedLengthMatcher = new TypedAnythingMatcher<>(); 34 | private Matcher expectedLoggerNameMatcher = new TypedAnythingMatcher<>(); 35 | private ExpectedLoggedException expectedLoggedException = ExpectedLoggedException.ANYTHING; 36 | private Map> mdcMatcher = new HashMap<>(); 37 | 38 | private ExpectedLoggingMessage() { 39 | } 40 | 41 | public static ExpectedLoggingMessage aLog() { 42 | return new ExpectedLoggingMessage(); 43 | } 44 | 45 | public ExpectedLoggingMessage withLevel(Matcher errorLevel) { 46 | logLevelMatcher = errorLevel; 47 | return this; 48 | } 49 | 50 | public ExpectedLoggingMessage withLevel(Level errorLevel) { 51 | return withLevel(equalTo(errorLevel)); 52 | } 53 | 54 | public ExpectedLoggingMessage withMarker(Matcher marker) { 55 | markerMatcher = marker; 56 | return this; 57 | } 58 | 59 | @Override 60 | protected boolean matchesSafely(List events) { 61 | return events.stream().anyMatch(this::matches); 62 | } 63 | 64 | private boolean matches(ILoggingEvent event) { 65 | return logLevelMatcher.matches(event.getLevel()) && 66 | markerMatcher.matches(event.getMarker()) && 67 | expectedMessageMatcher.stream().allMatch(matcher -> matcher.matches(event.getFormattedMessage())) && 68 | expectedLoggedException.matches(event) && 69 | expectedLoggerNameMatcher.matches(event.getLoggerName()) && 70 | expectedLengthMatcher.matches(event.getFormattedMessage().length()) && 71 | matchesMdc(event.getMDCPropertyMap()); 72 | } 73 | 74 | public ExpectedLoggingMessage withMarker(Marker marker) { 75 | return withMarker(equalTo(marker)); 76 | } 77 | 78 | public ExpectedLoggingMessage withMarker(String marker) { 79 | return withMarker(MarkerFactory.getMarker(marker)); 80 | } 81 | 82 | public ExpectedLoggingMessage debug() { 83 | logLevelMatcher = equalTo(DEBUG); 84 | return this; 85 | } 86 | 87 | public ExpectedLoggingMessage info() { 88 | logLevelMatcher = equalTo(INFO); 89 | return this; 90 | } 91 | 92 | public ExpectedLoggingMessage warn() { 93 | logLevelMatcher = equalTo(WARN); 94 | return this; 95 | } 96 | 97 | public ExpectedLoggingMessage error() { 98 | logLevelMatcher = equalTo(ERROR); 99 | return this; 100 | } 101 | 102 | private boolean matchesMdc(Map mdcPropertyMap) { 103 | return mdcMatcher.entrySet().stream() 104 | .allMatch(entry -> entry.getValue().matches(mdcPropertyMap.get(entry.getKey()))); 105 | } 106 | 107 | public final ExpectedLoggingMessage withMessage(Matcher expectedMessages) { 108 | return withMessage(singleton(expectedMessages)); 109 | } 110 | 111 | @SafeVarargs 112 | public final ExpectedLoggingMessage withMessage(Matcher... expectedMessages) { 113 | return withMessage(Arrays.asList(expectedMessages)); 114 | } 115 | 116 | public final ExpectedLoggingMessage withMessage(Collection> expectedMessages) { 117 | expectedMessageMatcher.addAll(expectedMessages); 118 | return this; 119 | } 120 | 121 | public ExpectedLoggingMessage withMessage(String expectedMessage) { 122 | return withMessage(equalTo(expectedMessage)); 123 | } 124 | 125 | public ExpectedLoggingMessage withLoggerName(Matcher expectedLoggerName) { 126 | expectedLoggerNameMatcher = expectedLoggerName; 127 | return this; 128 | } 129 | 130 | public ExpectedLoggingMessage havingException(ExpectedLoggedException expectedLoggedException) { 131 | this.expectedLoggedException = expectedLoggedException; 132 | return this; 133 | } 134 | 135 | public ExpectedLoggingMessage length(Matcher expectedLengthMatcher) { 136 | this.expectedLengthMatcher = expectedLengthMatcher; 137 | return this; 138 | } 139 | 140 | public ExpectedLoggingMessage withMdc(String mdcKey, String mdcValue) { 141 | withMdc(mdcKey, equalTo(mdcValue)); 142 | return this; 143 | } 144 | 145 | public ExpectedLoggingMessage withMdc(String mdcKey, Matcher mdcValue) { 146 | mdcMatcher.put(mdcKey, mdcValue); 147 | return this; 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return "ExpectedLoggingMessage{" + description() + '}'; 153 | } 154 | 155 | private String description() { 156 | List results = new ArrayList<>(); 157 | 158 | results.addAll(toList("logLevelMatcher", logLevelMatcher)); 159 | results.addAll(toList("markerMatcher", markerMatcher)); 160 | results.addAll(toList("expectedMessageMatcher", expectedMessageMatcher)); 161 | results.addAll(toList("expectedLengthMatcher", expectedLengthMatcher)); 162 | results.addAll(toList("expectedLoggerNameMatcher", expectedLoggerNameMatcher)); 163 | results.addAll(toList("expectedMdc", mdcMatcher)); 164 | 165 | if (!(expectedLoggedException == ExpectedLoggedException.ANYTHING)) { 166 | results.add("expectedLoggedException" + "=" + expectedLoggedException); 167 | } 168 | 169 | return results.stream().collect(Collectors.joining(", ")); 170 | } 171 | 172 | private List toList(String fieldName, Object field) { 173 | List description = new ArrayList<>(); 174 | if (!(field instanceof TypedAnythingMatcher)) { 175 | description.add(fieldName + "=" + field); 176 | } 177 | return description; 178 | } 179 | 180 | @Override 181 | public void describeTo(Description description) { 182 | description.appendText(description()); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/assertion/VerificationException.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.assertion; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import org.hamcrest.Matcher; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | public class VerificationException extends AssertionError { 10 | 11 | private VerificationException(String message) { 12 | super(message); 13 | } 14 | 15 | public static VerificationException forUnmatchedLog(Matcher> expectedLogMessage, List logEvents) { 16 | return new VerificationException(String.format( 17 | "Expected matching: \n%s\nLogs received: \n%s", 18 | expectedLogMessage.toString(), 19 | logEvents.stream() 20 | .map(VerificationException::formatLogEvent) 21 | .collect(Collectors.joining("\n")) 22 | )); 23 | } 24 | 25 | public static VerificationException forUnmatchedTimesLog(Matcher> expectedLogMessage, List logEvents, Integer expectedTimes, Integer times) { 26 | return new VerificationException(String.format( 27 | "Expected %d times but got %d: \n%s\nLogs matched: \n%s", 28 | expectedTimes, 29 | times, 30 | expectedLogMessage.toString(), 31 | logEvents.stream() 32 | .map(VerificationException::formatLogEvent) 33 | .collect(Collectors.joining("\n")) 34 | )); 35 | } 36 | 37 | private static String formatLogEvent(ILoggingEvent log) { 38 | return String.format("level: %s marker: %s mdc: %s message: %s", log.getLevel(), 39 | log.getMarker(), 40 | log.getMDCPropertyMap(), 41 | log.getFormattedMessage()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/logback/StubAppender.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.logback; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.Appender; 5 | import ch.qos.logback.core.Context; 6 | import ch.qos.logback.core.LogbackException; 7 | import ch.qos.logback.core.filter.Filter; 8 | import ch.qos.logback.core.spi.FilterReply; 9 | import ch.qos.logback.core.status.Status; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.CopyOnWriteArrayList; 13 | 14 | public class StubAppender implements Appender { 15 | public static final String STUB_APPENDER_NAME = "stub-appender"; 16 | private final List loggedEvents = new CopyOnWriteArrayList<>(); 17 | 18 | @Override 19 | public String getName() { 20 | return STUB_APPENDER_NAME; 21 | } 22 | 23 | @Override 24 | public void doAppend(ILoggingEvent iLoggingEvent) throws LogbackException { 25 | loggedEvents.add(iLoggingEvent); 26 | } 27 | 28 | @Override 29 | public void setName(String s) { 30 | } 31 | 32 | @Override 33 | public void setContext(Context context) { 34 | } 35 | 36 | @Override 37 | public Context getContext() { 38 | return null; 39 | } 40 | 41 | @Override 42 | public void addStatus(Status status) { 43 | } 44 | 45 | @Override 46 | public void addInfo(String s) { 47 | } 48 | 49 | @Override 50 | public void addInfo(String s, Throwable throwable) { 51 | } 52 | 53 | @Override 54 | public void addWarn(String s) { 55 | } 56 | 57 | @Override 58 | public void addWarn(String s, Throwable throwable) { 59 | } 60 | 61 | @Override 62 | public void addError(String s) { 63 | } 64 | 65 | @Override 66 | public void addError(String s, Throwable throwable) { 67 | } 68 | 69 | @Override 70 | public void addFilter(Filter filter) { 71 | 72 | } 73 | 74 | @Override 75 | public void clearAllFilters() { 76 | loggedEvents.clear(); 77 | } 78 | 79 | @Override 80 | public List> getCopyOfAttachedFiltersList() { 81 | return null; 82 | } 83 | 84 | @Override 85 | public FilterReply getFilterChainDecision(ILoggingEvent iLoggingEvent) { 86 | return null; 87 | } 88 | 89 | @Override 90 | public void start() { 91 | } 92 | 93 | @Override 94 | public void stop() { 95 | } 96 | 97 | @Override 98 | public boolean isStarted() { 99 | return true; 100 | } 101 | 102 | public List events() { 103 | return loggedEvents; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/matcher/ExpectedExceptionMatcher.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher; 2 | 3 | import ch.qos.logback.classic.spi.ThrowableProxy; 4 | import org.hamcrest.BaseMatcher; 5 | import org.hamcrest.Description; 6 | import org.hamcrest.Matcher; 7 | 8 | public class ExpectedExceptionMatcher extends BaseMatcher { 9 | private final Matcher expectedException; 10 | 11 | public ExpectedExceptionMatcher(Matcher expectedException) { 12 | this.expectedException = expectedException; 13 | } 14 | 15 | @Override 16 | public boolean matches(Object item) { 17 | if (item instanceof ThrowableProxy) { 18 | return expectedException.matches(((ThrowableProxy) item).getThrowable()); 19 | } 20 | 21 | return false; 22 | } 23 | 24 | @Override 25 | public void describeTo(Description description) { 26 | expectedException.describeTo(description); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/matcher/TypedAnythingMatcher.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher; 2 | 3 | import org.hamcrest.BaseMatcher; 4 | import org.hamcrest.Description; 5 | 6 | public class TypedAnythingMatcher extends BaseMatcher { 7 | 8 | @Override 9 | public boolean matches(Object item) { 10 | return true; 11 | } 12 | 13 | @Override 14 | public void describeTo(Description description) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/matcher/exception/ExceptionCauseMatcher.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher.exception; 2 | 3 | import org.hamcrest.BaseMatcher; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | 7 | public class ExceptionCauseMatcher extends BaseMatcher { 8 | 9 | private final Class exceptionClass; 10 | 11 | private ExceptionCauseMatcher(Class exceptionClass) { 12 | this.exceptionClass = exceptionClass; 13 | } 14 | 15 | public static Matcher causeOf(Class exceptionClass) { 16 | return new ExceptionCauseMatcher(exceptionClass); 17 | } 18 | 19 | @Override 20 | public boolean matches(Object exception) { 21 | Throwable expectedExceptionCause = ((Throwable) exception).getCause(); 22 | return exceptionClass.isInstance(expectedExceptionCause); 23 | } 24 | 25 | @Override 26 | public void describeTo(Description description) { 27 | description.appendText("Expecting exception to be instance of " + exceptionClass); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /logcapture-core/src/main/java/org/logcapture/matcher/exception/ExceptionCauseMessageMatcher.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher.exception; 2 | 3 | import org.hamcrest.BaseMatcher; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | 7 | public class ExceptionCauseMessageMatcher extends BaseMatcher { 8 | 9 | private final Matcher causeMatcher; 10 | 11 | private ExceptionCauseMessageMatcher(Matcher causeMatcher) { 12 | this.causeMatcher = causeMatcher; 13 | } 14 | 15 | public static Matcher whereCauseMessage(Matcher causeMatcher) { 16 | return new ExceptionCauseMessageMatcher(causeMatcher); 17 | } 18 | 19 | @Override 20 | public boolean matches(Object exception) { 21 | final Throwable expectedExceptionCause = ((Throwable) exception).getCause(); 22 | return causeMatcher.matches(expectedExceptionCause.getMessage()); 23 | } 24 | 25 | @Override 26 | public void describeTo(Description description) { 27 | description.appendText("Expecting exception cause to contain " + causeMatcher); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/LogCaptureShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.Logger; 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import ch.qos.logback.classic.spi.LoggingEvent; 7 | import org.assertj.core.api.Assertions; 8 | import org.junit.Test; 9 | import org.logcapture.assertion.ExpectedLoggingMessage; 10 | import org.logcapture.assertion.VerificationException; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.Arrays; 14 | 15 | import static ch.qos.logback.classic.Level.INFO; 16 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 17 | 18 | public class LogCaptureShould { 19 | 20 | @Test 21 | public void match_n_times() { 22 | LoggingEvent log1 = aLoggingEventWith(INFO, "message"); 23 | 24 | LogCapture underTest = new LogCapture<>(Arrays.asList(log1, log1)); 25 | 26 | underTest.logged(aLog().withMessage("message"), 2); 27 | } 28 | 29 | @Test 30 | public void match_n_times_filtering_others() { 31 | LoggingEvent log1 = aLoggingEventWith(INFO, "message"); 32 | LoggingEvent log2 = aLoggingEventWith(INFO, "another"); 33 | 34 | LogCapture underTest = new LogCapture<>(Arrays.asList(log1, log1, log2)); 35 | 36 | ExpectedLoggingMessage expectedLog = aLog().withMessage("message"); 37 | underTest.logged(expectedLog, 2); 38 | } 39 | 40 | @Test 41 | public void match_n_times_multiples_messages() { 42 | LoggingEvent log1 = aLoggingEventWith(INFO, "message"); 43 | LoggingEvent log2 = aLoggingEventWith(INFO, "another-message"); 44 | 45 | LogCapture underTest = new LogCapture<>(Arrays.asList(log1, log2, log1, log2)); 46 | 47 | underTest.logged(aLog().withMessage("message"), 2); 48 | } 49 | 50 | @Test 51 | public void fail_matching_n_times() { 52 | LoggingEvent log1 = aLoggingEventWith(INFO, "message"); 53 | 54 | LogCapture underTest = new LogCapture<>(Arrays.asList(log1, log1)); 55 | 56 | Assertions.assertThatThrownBy( 57 | () -> underTest.logged(aLog().withMessage("message"), 1) 58 | ).isInstanceOf(VerificationException.class) 59 | .hasMessageContaining("Expected 1 times but got 2:"); 60 | 61 | } 62 | 63 | private LoggingEvent aLoggingEventWith(Level level, String message) { 64 | return aLoggingEventWith(level, message, null); 65 | } 66 | 67 | private LoggingEvent aLoggingEventWith(Level level, String message, Exception exception) { 68 | Logger log = (Logger) LoggerFactory.getLogger(this.getClass()); 69 | return new LoggingEvent("fqcn", log, level, message, exception, null); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/assertion/ExpectedLoggedExceptionShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.assertion; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.Logger; 5 | import ch.qos.logback.classic.spi.LoggingEvent; 6 | import org.junit.Test; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import static ch.qos.logback.classic.Level.INFO; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.hamcrest.Matchers.equalTo; 12 | import static org.hamcrest.Matchers.isA; 13 | 14 | public class ExpectedLoggedExceptionShould { 15 | 16 | @Test 17 | public void match_when_exception_class_match() { 18 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message", new IllegalArgumentException()); 19 | ExpectedLoggedException expectedLoggedException = ExpectedLoggedException.logException() 20 | .withException(isA(IllegalArgumentException.class)); 21 | 22 | boolean matches = expectedLoggedException.matches(logEvent); 23 | 24 | assertThat(matches).isTrue(); 25 | } 26 | 27 | @Test 28 | public void match_when_exception_message_match() { 29 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message", new IllegalArgumentException("message error")); 30 | ExpectedLoggedException expectedLoggedException = ExpectedLoggedException.logException() 31 | .withMessage(equalTo("message error")); 32 | 33 | boolean matches = expectedLoggedException.matches(logEvent); 34 | 35 | assertThat(matches).isTrue(); 36 | } 37 | 38 | @Test 39 | public void not_match_when_exception_class_different() { 40 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message", new IllegalArgumentException()); 41 | ExpectedLoggedException expectedLoggedException = ExpectedLoggedException.logException() 42 | .withException(isA(IllegalStateException.class)); 43 | 44 | boolean matches = expectedLoggedException.matches(logEvent); 45 | 46 | assertThat(matches).isFalse(); 47 | } 48 | 49 | @Test 50 | public void not_match_when_exception_message_different() { 51 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message", new IllegalArgumentException("some error")); 52 | ExpectedLoggedException expectedLoggedException = ExpectedLoggedException.logException() 53 | .withMessage(equalTo("another error")); 54 | 55 | boolean matches = expectedLoggedException.matches(logEvent); 56 | 57 | assertThat(matches).isFalse(); 58 | } 59 | 60 | @Test 61 | public void to_string_method_when_exception_not_match() { 62 | ExpectedLoggedException expectedLoggedException = ExpectedLoggedException.logException() 63 | .withMessage(equalTo("another error")) 64 | .withException(isA(IllegalArgumentException.class)); 65 | 66 | assertThat(expectedLoggedException.toString()).isEqualTo("ExpectedLoggedException{expectedMessageMatcher=\"another error\", expectedException=is an instance of java.lang.IllegalArgumentException}"); 67 | } 68 | 69 | private LoggingEvent aLoggingEventWith(Level level, String message, Exception exception) { 70 | Logger log = (Logger) LoggerFactory.getLogger(this.getClass()); 71 | return new LoggingEvent("fqcn", log, level, message, exception, null); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/assertion/ExpectedLoggingMessageShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.assertion; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.Logger; 5 | import ch.qos.logback.classic.spi.LoggingEvent; 6 | import org.junit.Test; 7 | import org.slf4j.LoggerFactory; 8 | import org.slf4j.MDC; 9 | import org.slf4j.MarkerFactory; 10 | 11 | import static ch.qos.logback.classic.Level.DEBUG; 12 | import static ch.qos.logback.classic.Level.ERROR; 13 | import static ch.qos.logback.classic.Level.INFO; 14 | import static ch.qos.logback.classic.Level.WARN; 15 | import static org.logcapture.assertion.ExpectedLoggedException.logException; 16 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 17 | import static org.logcapture.matcher.exception.ExceptionCauseMatcher.causeOf; 18 | import static java.util.Arrays.asList; 19 | import static java.util.Collections.emptyList; 20 | import static java.util.Collections.singletonList; 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.hamcrest.Matchers.containsString; 23 | import static org.hamcrest.Matchers.equalTo; 24 | import static org.hamcrest.Matchers.instanceOf; 25 | 26 | public class ExpectedLoggingMessageShould { 27 | 28 | @Test 29 | public void match_when_at_least_one_element_is_matching() { 30 | LoggingEvent matchingEvent = aLoggingEventWith(INFO, "message"); 31 | LoggingEvent notMatchingEvent = aLoggingEventWith(INFO, "message"); 32 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withLevel(equalTo(INFO)); 33 | 34 | boolean matches = expectedLoggingMessage.matches(asList(matchingEvent, notMatchingEvent)); 35 | 36 | assertThat(matches).isTrue(); 37 | } 38 | 39 | @Test 40 | public void no_match_when_there_are_no_events() { 41 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withLevel(equalTo(INFO)); 42 | 43 | boolean matches = expectedLoggingMessage.matches(emptyList()); 44 | 45 | assertThat(matches).isFalse(); 46 | } 47 | 48 | @Test 49 | public void not_match_when_no_element_is_matching() { 50 | LoggingEvent notMatchingEvent1 = aLoggingEventWith(INFO, "message"); 51 | LoggingEvent notMatchingEvent2 = aLoggingEventWith(INFO, "message"); 52 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withLevel(equalTo(DEBUG)); 53 | 54 | boolean matches = expectedLoggingMessage.matches(asList(notMatchingEvent1, notMatchingEvent2)); 55 | 56 | assertThat(matches).isFalse(); 57 | } 58 | 59 | @Test 60 | public void match_when_log_level_match() { 61 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 62 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withLevel(equalTo(INFO)); 63 | 64 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 65 | 66 | assertThat(matches).isTrue(); 67 | } 68 | 69 | @Test 70 | public void match_when_debug_log_level_match() { 71 | LoggingEvent logEvent = aLoggingEventWith(DEBUG, "message"); 72 | ExpectedLoggingMessage expectedLoggingMessage = aLog().debug(); 73 | 74 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 75 | 76 | assertThat(matches).isTrue(); 77 | } 78 | 79 | @Test 80 | public void match_when_info_log_level_match() { 81 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 82 | ExpectedLoggingMessage expectedLoggingMessage = aLog().info(); 83 | 84 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 85 | 86 | assertThat(matches).isTrue(); 87 | } 88 | 89 | @Test 90 | public void match_when_warn_log_level_match() { 91 | LoggingEvent logEvent = aLoggingEventWith(WARN, "message"); 92 | ExpectedLoggingMessage expectedLoggingMessage = aLog().warn(); 93 | 94 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 95 | 96 | assertThat(matches).isTrue(); 97 | } 98 | 99 | @Test 100 | public void match_when_error_log_level_match() { 101 | LoggingEvent logEvent = aLoggingEventWith(ERROR, "message"); 102 | ExpectedLoggingMessage expectedLoggingMessage = aLog().error(); 103 | 104 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 105 | 106 | assertThat(matches).isTrue(); 107 | } 108 | 109 | @Test 110 | public void match_when_message_match() { 111 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 112 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withMessage(equalTo("message")); 113 | 114 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 115 | 116 | assertThat(matches).isTrue(); 117 | } 118 | 119 | @Test 120 | public void match_when_multiple_messages_matches() { 121 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message has another message"); 122 | 123 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withMessage(containsString("message"), 124 | containsString("has"), 125 | containsString("another") 126 | ); 127 | 128 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 129 | 130 | assertThat(matches).isTrue(); 131 | } 132 | 133 | @Test 134 | public void not_match_when_a_message_not_matches() { 135 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message has another message"); 136 | 137 | ExpectedLoggingMessage expectedLoggingMessage = aLog().withMessage(containsString("message"), 138 | containsString("has"), 139 | containsString("NO_MATCH"), 140 | containsString("another") 141 | ); 142 | 143 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 144 | 145 | assertThat(matches).isFalse(); 146 | } 147 | 148 | @Test 149 | public void match_when_length_message_match() { 150 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 151 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 152 | .length(equalTo(7)); 153 | 154 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 155 | 156 | assertThat(matches).isTrue(); 157 | } 158 | 159 | @Test 160 | public void match_when_mdc_keys_match() { 161 | MDC.put("aKey", "someValue"); 162 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 163 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 164 | .withMdc("aKey", equalTo("someValue")); 165 | 166 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 167 | 168 | assertThat(matches).isTrue(); 169 | } 170 | 171 | @Test 172 | public void match_when_multiple_mdc_keys_match() { 173 | MDC.put("aKey", "someValue"); 174 | MDC.put("anotherKey", "anotherValue"); 175 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 176 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 177 | .withMdc("aKey", equalTo("someValue")) 178 | .withMdc("anotherKey", equalTo("anotherValue")); 179 | 180 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 181 | 182 | assertThat(matches).isTrue(); 183 | } 184 | 185 | @Test 186 | public void match_when_logger_class_match() { 187 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 188 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 189 | .withLoggerName(equalTo(ExpectedLoggingMessageShould.class.getName())); 190 | 191 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 192 | 193 | assertThat(matches).isTrue(); 194 | } 195 | 196 | @Test 197 | public void match_when_exception_class_match() { 198 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message", new RuntimeException()); 199 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 200 | .havingException(logException() 201 | .withException(instanceOf(RuntimeException.class))); 202 | 203 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 204 | 205 | assertThat(matches).isTrue(); 206 | } 207 | 208 | @Test 209 | public void match_when_message_and_log_level_match() { 210 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 211 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 212 | .withLevel(equalTo(INFO)) 213 | .withMessage(equalTo("message")); 214 | 215 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 216 | 217 | assertThat(matches).isTrue(); 218 | } 219 | 220 | @Test 221 | public void not_match_when_log_level_different() { 222 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 223 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 224 | .withMessage(equalTo("differentMessage")); 225 | 226 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 227 | 228 | assertThat(matches).isFalse(); 229 | } 230 | 231 | @Test 232 | public void not_match_when_message_different() { 233 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 234 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 235 | .withMessage(equalTo("anotherMessage")); 236 | 237 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 238 | 239 | assertThat(matches).isFalse(); 240 | } 241 | 242 | @Test 243 | public void not_match_when_message_length_different() { 244 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 245 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 246 | .length(equalTo(8)); 247 | 248 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 249 | 250 | assertThat(matches).isFalse(); 251 | } 252 | 253 | @Test 254 | public void not_match_when_mdc_keys_different() { 255 | MDC.put("aKey", "differentValue"); 256 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 257 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 258 | .withMdc("aKey", equalTo("someValue")); 259 | 260 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 261 | 262 | MDC.clear(); 263 | assertThat(matches).isFalse(); 264 | } 265 | 266 | @Test 267 | public void not_match_when_a_mdc_keys_is_different() { 268 | MDC.put("aKey", "differentValue"); 269 | MDC.put("anotherKey", "anotherValue"); 270 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 271 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 272 | .withMdc("aKey", equalTo("unmatchedValue")) 273 | .withMdc("anotherKey", equalTo("anotherValue")); 274 | 275 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 276 | 277 | MDC.clear(); 278 | assertThat(matches).isFalse(); 279 | } 280 | 281 | @Test 282 | public void not_match_when_logger_class_different() { 283 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 284 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 285 | .withLoggerName(equalTo("anotherClassName")); 286 | 287 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 288 | 289 | assertThat(matches).isFalse(); 290 | } 291 | 292 | @Test 293 | public void match_for_expected_marker() { 294 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 295 | logEvent.addMarker(MarkerFactory.getMarker("A_MARKER")); 296 | 297 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 298 | .withMarker(MarkerFactory.getMarker("A_MARKER")); 299 | 300 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 301 | 302 | assertThat(matches).isTrue(); 303 | } 304 | 305 | @Test 306 | public void match_for_expected_marker_label() { 307 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 308 | logEvent.addMarker(MarkerFactory.getMarker("A_MARKER")); 309 | 310 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 311 | .withMarker("A_MARKER"); 312 | 313 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 314 | 315 | assertThat(matches).isTrue(); 316 | } 317 | 318 | @Test 319 | public void no_match_for_unexpected_marker() { 320 | LoggingEvent logEvent = aLoggingEventWith(INFO, "message"); 321 | logEvent.addMarker(MarkerFactory.getMarker("A_MARKER")); 322 | 323 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 324 | .withMarker("ANOTHER_MARKER"); 325 | 326 | boolean matches = expectedLoggingMessage.matches(singletonList(logEvent)); 327 | 328 | assertThat(matches).isFalse(); 329 | } 330 | 331 | @Test 332 | public void describe_failure_using_to_string() { 333 | ExpectedLoggingMessage expectedLoggingMessage = aLog() 334 | .withLevel(equalTo(ERROR)) 335 | .withMessage(equalTo("message")) 336 | .length(equalTo(8)) 337 | .withMdc("aKey", equalTo("some")) 338 | .withMarker("A_MARKER") 339 | .withLoggerName(equalTo("className")) 340 | .havingException(logException() 341 | .withMessage(equalTo("exception thrown")) 342 | .withException(causeOf(IllegalArgumentException.class))); 343 | 344 | assertThat(expectedLoggingMessage.toString()) 345 | .contains("logLevelMatcher=") 346 | .contains("markerMatcher=") 347 | .contains("expectedMessageMatcher=[\"message\"]") 348 | .contains("expectedLengthMatcher=<8>") 349 | .contains("expectedMdc={aKey=\"some\"}") 350 | .contains("expectedLoggerNameMatcher=\"className\"") 351 | .contains("expectedLoggedException=ExpectedLoggedException{expectedMessageMatcher=\"exception thrown\", expectedException=Expecting exception to be instance of class java.lang.IllegalArgumentException"); 352 | } 353 | 354 | private LoggingEvent aLoggingEventWith(Level level, String message) { 355 | return aLoggingEventWith(level, message, null); 356 | } 357 | 358 | private LoggingEvent aLoggingEventWith(Level level, String message, Exception exception) { 359 | Logger log = (Logger) LoggerFactory.getLogger(this.getClass()); 360 | return new LoggingEvent("fqcn", log, level, message, exception, null); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/assertion/VerificationExceptionShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.assertion; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.Logger; 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import ch.qos.logback.classic.spi.LoggingEvent; 7 | import org.junit.Test; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.Arrays; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 16 | import static java.util.Collections.emptyMap; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.hamcrest.Matchers.equalTo; 19 | 20 | public class VerificationExceptionShould { 21 | 22 | @Test 23 | public void containLogsEventsNotMatchingExpected() { 24 | ExpectedLoggingMessage expectedLogMessage = aLog().info().withMessage("a log message"); 25 | List logEvents = Arrays.asList( 26 | aLoggingEventWith(Level.INFO, "a different message"), 27 | aLoggingEventWith(Level.INFO, "another different message") 28 | ); 29 | 30 | VerificationException verificationException = VerificationException.forUnmatchedLog(expectedLogMessage, logEvents); 31 | 32 | assertThat(verificationException.toString()).isEqualTo("org.logcapture.assertion.VerificationException: Expected matching: \n" + 33 | "ExpectedLoggingMessage{logLevelMatcher=, expectedMessageMatcher=[\"a log message\"], expectedMdc={}}\n" + 34 | "Logs received: \n" + 35 | "level: INFO marker: null mdc: {} message: a different message\n" + 36 | "level: INFO marker: null mdc: {} message: another different message"); 37 | } 38 | 39 | @Test 40 | public void containLogsEventsNotMatchingExpectedWithMdcKeys() { 41 | Map mdcKeys = new HashMap() {{ 42 | put("aKey", "aValue"); 43 | put("anotherKey", "anotherValue"); 44 | }}; 45 | 46 | ExpectedLoggingMessage expectedLogMessage = aLog().info() 47 | .withMessage("a log message") 48 | .withMdc("aKey", equalTo("aValue")) 49 | .withMdc("anotherKey", equalTo("anotherValue")); 50 | 51 | List logEvents = Arrays.asList( 52 | aLoggingEventWith(Level.INFO, "a different message", mdcKeys), 53 | aLoggingEventWith(Level.INFO, "another different message", mdcKeys) 54 | ); 55 | 56 | VerificationException verificationException = VerificationException.forUnmatchedLog(expectedLogMessage, logEvents); 57 | 58 | assertThat(verificationException.toString()).isEqualTo("org.logcapture.assertion.VerificationException: Expected matching: \n" + 59 | "ExpectedLoggingMessage{logLevelMatcher=, expectedMessageMatcher=[\"a log message\"], expectedMdc={anotherKey=\"anotherValue\", aKey=\"aValue\"}}\n" + 60 | "Logs received: \n" + 61 | "level: INFO marker: null mdc: {anotherKey=anotherValue, aKey=aValue} message: a different message\n" + 62 | "level: INFO marker: null mdc: {anotherKey=anotherValue, aKey=aValue} message: another different message"); 63 | } 64 | 65 | private LoggingEvent aLoggingEventWith(Level level, String message) { 66 | return aLoggingEventWith(level, message, emptyMap()); 67 | } 68 | 69 | private LoggingEvent aLoggingEventWith(Level level, String message, Map mdcKeys) { 70 | Logger log = (Logger) LoggerFactory.getLogger(getClass()); 71 | LoggingEvent logEvent = new LoggingEvent("fqcn", log, level, message, null, null); 72 | logEvent.setMDCPropertyMap(mdcKeys); 73 | return logEvent; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/matcher/ExpectedExceptionMatcherShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher; 2 | 3 | import ch.qos.logback.classic.spi.ThrowableProxy; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.hamcrest.core.Is.isA; 8 | 9 | public class ExpectedExceptionMatcherShould { 10 | 11 | @Test 12 | public void match_when_ThrowableProxy_contains_exception() { 13 | ExpectedExceptionMatcher expectedExceptionMatcher = new ExpectedExceptionMatcher(isA(RuntimeException.class)); 14 | 15 | boolean matches = expectedExceptionMatcher.matches(new ThrowableProxy(new RuntimeException())); 16 | 17 | assertThat(matches).isTrue(); 18 | } 19 | 20 | @Test 21 | public void not_match_when_is_not_ThrowableProxy() { 22 | ExpectedExceptionMatcher expectedExceptionMatcher = new ExpectedExceptionMatcher(isA(RuntimeException.class)); 23 | 24 | boolean matches = expectedExceptionMatcher.matches(new RuntimeException()); 25 | 26 | assertThat(matches).isFalse(); 27 | } 28 | 29 | @Test 30 | public void not_match_when_ThrowableProxy_contains_another_exception() { 31 | ExpectedExceptionMatcher expectedExceptionMatcher = new ExpectedExceptionMatcher(isA(IllegalArgumentException.class)); 32 | 33 | boolean matches = expectedExceptionMatcher.matches(new ThrowableProxy(new IllegalStateException())); 34 | 35 | assertThat(matches).isFalse(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/matcher/exception/ExceptionCauseMatcherShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher.exception; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.hamcrest.StringDescription; 5 | import org.junit.Test; 6 | 7 | import static org.logcapture.matcher.exception.ExceptionCauseMatcher.causeOf; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class ExceptionCauseMatcherShould { 11 | 12 | @Test 13 | public void match_when_cause_matches() { 14 | Matcher matcher = causeOf(IllegalStateException.class); 15 | 16 | assertThat(matcher.matches(new Throwable(new IllegalStateException()))).isTrue(); 17 | } 18 | 19 | @Test 20 | public void not_match_when_cause_different() { 21 | Matcher matcher = causeOf(IllegalStateException.class); 22 | 23 | assertThat(matcher.matches(new Throwable(new IllegalArgumentException()))).isFalse(); 24 | } 25 | 26 | @Test 27 | public void description_adds_context() { 28 | Matcher matcher = causeOf(RuntimeException.class); 29 | StringDescription description = new StringDescription(); 30 | 31 | matcher.describeTo(description); 32 | 33 | assertThat(description.toString()).isEqualTo("Expecting exception to be instance of class java.lang.RuntimeException"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /logcapture-core/src/test/java/org/logcapture/matcher/exception/ExceptionCauseMessageMatcherShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.matcher.exception; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.hamcrest.StringDescription; 5 | import org.junit.Test; 6 | 7 | import static org.logcapture.matcher.exception.ExceptionCauseMessageMatcher.whereCauseMessage; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.hamcrest.CoreMatchers.equalTo; 10 | 11 | public class ExceptionCauseMessageMatcherShould { 12 | 13 | @Test 14 | public void match_when_message_matches() { 15 | Matcher matcher = whereCauseMessage(equalTo("message")); 16 | 17 | assertThat(matcher.matches(new Throwable(new RuntimeException("message")))).isTrue(); 18 | } 19 | 20 | @Test 21 | public void not_match_when_message_different() { 22 | Matcher matcher = whereCauseMessage(equalTo("message")); 23 | 24 | assertThat(matcher.matches(new Throwable(new RuntimeException("different message")))).isFalse(); 25 | } 26 | 27 | @Test 28 | public void description_adds_context() { 29 | Matcher matcher = whereCauseMessage(equalTo("message")); 30 | StringDescription description = new StringDescription(); 31 | 32 | matcher.describeTo(description); 33 | 34 | assertThat(description.toString()).isEqualTo("Expecting exception cause to contain \"message\""); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /logcapture-example/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | implementation "org.logcapture:logcapture-junit4:1.3.6" 4 | implementation "junit:junit:4.13.2" 5 | } 6 | -------------------------------------------------------------------------------- /logcapture-example/src/test/java/org/logcapture/example/ExampleShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.example; 2 | 3 | import org.logcapture.junit4.LogCaptureRule; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.slf4j.MDC; 9 | import org.slf4j.MarkerFactory; 10 | 11 | import static ch.qos.logback.classic.Level.INFO; 12 | import static org.logcapture.assertion.ExpectedLoggedException.logException; 13 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 14 | import static org.logcapture.matcher.exception.ExceptionCauseMatcher.causeOf; 15 | import static org.logcapture.matcher.exception.ExceptionCauseMessageMatcher.whereCauseMessage; 16 | import static org.hamcrest.Matchers.containsString; 17 | import static org.hamcrest.Matchers.equalTo; 18 | import static org.hamcrest.Matchers.isA; 19 | 20 | public class ExampleShould { 21 | private final Logger log = LoggerFactory.getLogger(ExampleShould.class); 22 | 23 | @Rule 24 | public LogCaptureRule logCaptureRule = new LogCaptureRule(); 25 | 26 | private ServiceThatLogs underTest = new ServiceThatLogs(); 27 | 28 | @Test 29 | public void verify_captured_events() { 30 | underTest.methodThatLogsStuff(); 31 | logCaptureRule.logged(aLog().info() 32 | .withMessage("a message")); 33 | } 34 | 35 | @Test 36 | public void verify_captured_events_with_marker() { 37 | log.info(MarkerFactory.getMarker("a_marker"), "a message"); 38 | logCaptureRule.logged(aLog() 39 | .withLevel(equalTo(INFO)) 40 | .withMarker("a_marker") 41 | .withMessage(equalTo("a message"))); 42 | } 43 | 44 | @Test 45 | public void verify_captured_events_with_exception() { 46 | RuntimeException exception = new RuntimeException(); 47 | 48 | underTest.methodThatLogs(exception); 49 | logCaptureRule.logged(aLog() 50 | .havingException(logException() 51 | .withException(isA(RuntimeException.class)) 52 | )); 53 | } 54 | 55 | @Test 56 | public void verify_captured_events_with_exception_cause_message() { 57 | RuntimeException exception = new RuntimeException(new IllegalStateException("Some state is invalid")); 58 | underTest.methodThatLogs(exception); 59 | logCaptureRule.logged(aLog() 60 | .havingException(logException() 61 | .withException(whereCauseMessage(containsString("state is invalid"))) 62 | )); 63 | } 64 | 65 | @Test 66 | public void verify_captured_events_with_exception_cause() { 67 | RuntimeException exception = new RuntimeException(new IllegalStateException("Some state is invalid")); 68 | underTest.methodThatLogs(exception); 69 | logCaptureRule.logged(aLog() 70 | .havingException(logException() 71 | .withException(causeOf(IllegalStateException.class)) 72 | )); 73 | } 74 | 75 | @Test 76 | public void verify_mdc_keys() { 77 | MDC.put("aKey", "someValue"); 78 | 79 | underTest.methodThatLogsStuff(); 80 | logCaptureRule.logged(aLog().info() 81 | .withMdc("aKey", equalTo("someValue")) 82 | .withMessage("a message")); 83 | } 84 | 85 | @Test 86 | public void verify_captured_n_logs() { 87 | log.info("a message"); 88 | log.info("a message"); 89 | log.info("what"); 90 | 91 | logCaptureRule.logged(aLog().info() 92 | .withMessage("a message"), 2); 93 | } 94 | 95 | class ServiceThatLogs { 96 | 97 | void methodThatLogsStuff() { 98 | log.info("a message"); 99 | log.info("another message"); 100 | } 101 | 102 | void methodThatLogs(Exception exception) { 103 | log.error("message", exception); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /logcapture-junit4/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | dependencies { 6 | api project(":logcapture-core") 7 | implementation "junit:junit:4.13.2" 8 | 9 | testImplementation "org.assertj:assertj-core:3.27.3" 10 | } 11 | -------------------------------------------------------------------------------- /logcapture-junit4/src/main/java/org/logcapture/junit4/LogCaptureRule.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.junit4; 2 | 3 | import ch.qos.logback.classic.Logger; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import org.hamcrest.Matcher; 6 | import org.junit.rules.MethodRule; 7 | import org.junit.rules.TestRule; 8 | import org.junit.runner.Description; 9 | import org.junit.runners.model.FrameworkMethod; 10 | import org.junit.runners.model.Statement; 11 | import org.logcapture.LogCapture; 12 | import org.logcapture.logback.StubAppender; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.List; 16 | 17 | import static org.slf4j.Logger.ROOT_LOGGER_NAME; 18 | 19 | public class LogCaptureRule implements MethodRule, TestRule { 20 | 21 | private final String loggerName; 22 | private StubAppender logAppender; 23 | 24 | public LogCaptureRule() { 25 | this(ROOT_LOGGER_NAME); 26 | } 27 | 28 | public LogCaptureRule(String loggerName) { 29 | this.loggerName = loggerName; 30 | } 31 | 32 | @Override 33 | public Statement apply(Statement base, Description description) { 34 | return apply(base, null, null); 35 | } 36 | 37 | @Override 38 | public Statement apply(Statement base, FrameworkMethod method, Object target) { 39 | return new Statement() { 40 | @Override 41 | public void evaluate() throws Throwable { 42 | logAppender = new StubAppender(); 43 | Logger root = (Logger) LoggerFactory.getLogger(loggerName); 44 | 45 | root.addAppender(logAppender); 46 | try { 47 | base.evaluate(); 48 | } finally { 49 | root.detachAppender(logAppender); 50 | } 51 | } 52 | }; 53 | } 54 | 55 | public LogCapture logged(Matcher> expectedLoggingMessage) { 56 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage); 57 | } 58 | 59 | public LogCapture logged(Matcher> expectedLoggingMessage, Integer times) { 60 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage, times); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /logcapture-junit4/src/test/java/org/logcapture/junit4/LogCaptureClassRuleShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.junit4; 2 | 3 | import org.junit.ClassRule; 4 | import org.junit.Test; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 9 | 10 | public class LogCaptureClassRuleShould { 11 | private final Logger log = LoggerFactory.getLogger(LogCaptureClassRuleShould.class); 12 | 13 | @ClassRule 14 | public static LogCaptureRule logCaptureRule = new LogCaptureRule(); 15 | 16 | @Test 17 | public void verify_sync_logs_using_rule() { 18 | log.info("a message"); 19 | 20 | logCaptureRule.logged(aLog().info().withMessage("a message")); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /logcapture-junit4/src/test/java/org/logcapture/junit4/LogCaptureRuleShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.junit4; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.logcapture.assertion.VerificationException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.slf4j.MDC; 9 | import org.slf4j.MarkerFactory; 10 | 11 | import static ch.qos.logback.classic.Level.DEBUG; 12 | import static ch.qos.logback.classic.Level.INFO; 13 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 14 | import static org.hamcrest.Matchers.*; 15 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 16 | 17 | public class LogCaptureRuleShould { 18 | private static final String LOG_NAME = "aLogNotAttachedToRoot"; 19 | private final Logger log = LoggerFactory.getLogger(LogCaptureRuleShould.class); 20 | 21 | @Rule 22 | public LogCaptureRule logCaptureRule = new LogCaptureRule(); 23 | 24 | @Rule 25 | public LogCaptureRule logCaptureRuleAttached = new LogCaptureRule(LOG_NAME); 26 | 27 | @Test 28 | public void verify_missing_events() { 29 | logCaptureRule.logged(not(aLog() 30 | .withLevel(equalTo(INFO)) 31 | .withMessage(equalTo("missing message")))); 32 | } 33 | 34 | @Test 35 | public void verify_multiple_events() { 36 | log.info("first message"); 37 | log.debug("second message"); 38 | logCaptureRule.logged(allOf( 39 | aLog().withLevel(equalTo(INFO)).withMessage(equalTo("first message")), 40 | aLog().withLevel(equalTo(DEBUG)).withMessage(equalTo("second message")) 41 | )); 42 | } 43 | 44 | @Test 45 | public void verify_sync_logs_using_rule() { 46 | log.info("a message"); 47 | 48 | logCaptureRule.logged(aLog().info().withMessage("a message")); 49 | } 50 | 51 | @Test 52 | public void verify_captured_events_with_marker() { 53 | log.info(MarkerFactory.getMarker("a_marker"), "a message"); 54 | logCaptureRule.logged(aLog() 55 | .withLevel(equalTo(INFO)) 56 | .withMarker("a_marker") 57 | .withMessage(equalTo("a message"))); 58 | } 59 | 60 | @Test 61 | public void verify_log_when_is_not_in_root() { 62 | Logger logNotInRoot = createLogger(LOG_NAME); 63 | 64 | logNotInRoot.info("a message"); 65 | 66 | assertThatExceptionOfType(VerificationException.class) 67 | .isThrownBy(() -> logCaptureRule.logged(aLog().info().withMessage("a message"))); 68 | 69 | logCaptureRuleAttached.logged(aLog().info().withMessage("a message")); 70 | } 71 | 72 | @Test 73 | public void verify_log_with_mdc_keys() { 74 | MDC.put("a-key", "a-value"); 75 | log.info("a message"); 76 | 77 | logCaptureRule.logged(aLog() 78 | .info() 79 | .withMdc("a-key", "a-value") 80 | .withMessage("a message")); 81 | } 82 | 83 | @Test 84 | public void verify_log_n_times() { 85 | log.info("a message"); 86 | log.info("a message"); 87 | 88 | logCaptureRule.logged(aLog().info().withMessage("a message"), 2); 89 | } 90 | 91 | private Logger createLogger(String name) { 92 | ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(name); 93 | logger.setLevel(DEBUG); 94 | logger.setAdditive(false); 95 | return logger; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /logcapture-junit5/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | dependencies { 6 | api project(":logcapture-core") 7 | 8 | implementation(platform('org.junit:junit-bom:5.12.2')) 9 | implementation "org.junit.jupiter:junit-jupiter-api" 10 | 11 | testImplementation "org.junit.jupiter:junit-jupiter-engine" 12 | testImplementation "org.junit.platform:junit-platform-launcher" 13 | } 14 | 15 | test { 16 | useJUnitPlatform() 17 | } 18 | -------------------------------------------------------------------------------- /logcapture-junit5/src/main/java/org/logcapture/junit5/LogCaptureExtension.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.junit5; 2 | 3 | import ch.qos.logback.classic.Logger; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import org.hamcrest.Matcher; 6 | import org.junit.jupiter.api.extension.AfterEachCallback; 7 | import org.junit.jupiter.api.extension.BeforeEachCallback; 8 | import org.junit.jupiter.api.extension.ExtensionContext; 9 | import org.logcapture.LogCapture; 10 | import org.logcapture.logback.StubAppender; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.List; 14 | 15 | import static org.slf4j.Logger.ROOT_LOGGER_NAME; 16 | 17 | public class LogCaptureExtension implements BeforeEachCallback, AfterEachCallback { 18 | 19 | private final String loggerName; 20 | private StubAppender logAppender; 21 | private Logger root; 22 | 23 | public LogCaptureExtension() { 24 | this(ROOT_LOGGER_NAME); 25 | } 26 | 27 | public LogCaptureExtension(String loggerName) { 28 | this.loggerName = loggerName; 29 | } 30 | 31 | @Override 32 | public void beforeEach(ExtensionContext context) { 33 | logAppender = new StubAppender(); 34 | root = (Logger) LoggerFactory.getLogger(loggerName); 35 | 36 | root.addAppender(logAppender); 37 | } 38 | 39 | @Override 40 | public void afterEach(ExtensionContext context) { 41 | root.detachAppender(logAppender); 42 | } 43 | 44 | public LogCapture logged(Matcher> expectedLoggingMessage) { 45 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage); 46 | } 47 | 48 | public LogCapture logged(Matcher> expectedLoggingMessage, Integer times) { 49 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage, times); 50 | } 51 | 52 | public List getEvents() { 53 | return logAppender.events(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /logcapture-junit5/src/test/java/org/logcapture/junit5/LogCaptureRegisterExtensionShould.java: -------------------------------------------------------------------------------- 1 | package org.logcapture.junit5; 2 | 3 | import org.hamcrest.Matchers; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.RegisterExtension; 6 | import org.logcapture.assertion.ExpectedLoggingMessage; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import static ch.qos.logback.classic.Level.DEBUG; 11 | import static ch.qos.logback.classic.Level.INFO; 12 | import static org.hamcrest.Matchers.equalTo; 13 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog; 14 | 15 | class LogCaptureRegisterExtensionShould { 16 | private final Logger log = LoggerFactory.getLogger(LogCaptureRegisterExtensionShould.class); 17 | 18 | @RegisterExtension 19 | LogCaptureExtension logCaptureExtension = new LogCaptureExtension(); 20 | 21 | @Test 22 | void verify_missing_events() { 23 | logCaptureExtension.logged(Matchers.not(ExpectedLoggingMessage.aLog() 24 | .withLevel(equalTo(INFO)) 25 | .withMessage(equalTo("missing message")))); 26 | } 27 | 28 | @Test 29 | void verify_multiple_events() { 30 | log.info("first message"); 31 | log.debug("second message"); 32 | logCaptureExtension.logged( 33 | Matchers.allOf( 34 | ExpectedLoggingMessage.aLog().withLevel(equalTo(INFO)).withMessage(equalTo("first message")), 35 | ExpectedLoggingMessage.aLog().withLevel(equalTo(DEBUG)).withMessage(equalTo("second message")) 36 | ) 37 | ); 38 | } 39 | 40 | @Test 41 | void verify_sync_logs_using_rule() { 42 | log.info("a message"); 43 | 44 | logCaptureExtension.logged(ExpectedLoggingMessage.aLog().info().withMessage("a message")); 45 | } 46 | 47 | @Test 48 | void verify_log_n_times() { 49 | log.info("a message"); 50 | log.info("a message"); 51 | 52 | logCaptureExtension.logged(aLog().info().withMessage("a message"), 2); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /logcapture-kotest/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '2.1.20' 3 | } 4 | 5 | ext { 6 | kotestVersion = '5.9.1' 7 | } 8 | 9 | dependencies { 10 | api project(":logcapture-core") 11 | 12 | implementation "io.kotest:kotest-runner-junit5-jvm:$kotestVersion" 13 | implementation "io.kotest:kotest-assertions-core-jvm:$kotestVersion" 14 | } 15 | 16 | test { 17 | useJUnitPlatform() 18 | } 19 | -------------------------------------------------------------------------------- /logcapture-kotest/src/main/kotlin/org/logcapture/kotest/LogCaptureListener.kt: -------------------------------------------------------------------------------- 1 | package org.logcapture.kotest 2 | 3 | import ch.qos.logback.classic.Logger 4 | import ch.qos.logback.classic.spi.ILoggingEvent 5 | import org.logcapture.LogCapture 6 | import org.logcapture.logback.StubAppender 7 | import io.kotest.core.listeners.TestListener 8 | import io.kotest.core.test.TestCase 9 | import io.kotest.core.test.TestResult 10 | import org.hamcrest.Matcher 11 | import org.slf4j.Logger.ROOT_LOGGER_NAME 12 | import org.slf4j.LoggerFactory 13 | 14 | class LogCaptureListener(private val loggerName: String = ROOT_LOGGER_NAME) : TestListener { 15 | 16 | private lateinit var logAppender: StubAppender 17 | private lateinit var root: Logger 18 | 19 | override suspend fun beforeTest(testCase: TestCase) { 20 | logAppender = StubAppender() 21 | root = LoggerFactory.getLogger(loggerName) as Logger 22 | root.addAppender(logAppender) 23 | } 24 | 25 | override suspend fun afterTest(testCase: TestCase, result: TestResult) { 26 | root.detachAppender(logAppender) 27 | } 28 | 29 | fun logged(expectedLoggingMessage: Matcher>): LogCapture { 30 | return LogCapture(logAppender.events()).logged(expectedLoggingMessage) 31 | } 32 | 33 | fun logged(expectedLoggingMessage: Matcher>, times: Int): LogCapture { 34 | return LogCapture(logAppender.events()).logged(expectedLoggingMessage, times) 35 | } 36 | 37 | fun logged(): LogCapture { 38 | return LogCapture(logAppender.events()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /logcapture-kotest/src/test/kotlin/org/logcapture/kotest/LogCaptureListenerSpec.kt: -------------------------------------------------------------------------------- 1 | package org.logcapture.kotest 2 | 3 | import ch.qos.logback.classic.Level 4 | import io.kotest.assertions.timing.eventually 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.inspectors.forOne 7 | import io.kotest.matchers.shouldBe 8 | import io.kotest.matchers.string.shouldContain 9 | import org.logcapture.assertion.ExpectedLoggingMessage.aLog 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory 12 | import kotlin.time.Duration.Companion.seconds 13 | 14 | class LogCaptureListenerSpec : StringSpec({ 15 | 16 | val log: Logger = LoggerFactory.getLogger(LogCaptureListenerSpec::class.java) 17 | 18 | val logCaptureListener = LogCaptureListener() 19 | listener(logCaptureListener) 20 | 21 | "verify log messages" { 22 | log.info("a message") 23 | 24 | logCaptureListener.logged(aLog().info().withMessage("a message")) 25 | } 26 | 27 | "verify log n times " { 28 | log.info("a message") 29 | log.info("a message") 30 | 31 | logCaptureListener.logged(aLog().info().withMessage("a message"), 2) 32 | } 33 | 34 | "use eventually to verify logs" { 35 | var i = 0 36 | eventually(1.seconds) { 37 | i += 1 38 | logMessageWhenCondition(log, i == 5) 39 | logCaptureListener.logged(aLog().info().withMessage("a message")) 40 | } 41 | } 42 | 43 | "verify using kotest assertions" { 44 | log.info("a message") 45 | 46 | logCaptureListener.logged().events.forOne { 47 | it.level shouldBe Level.INFO 48 | it.message shouldContain "message" 49 | } 50 | } 51 | }) 52 | 53 | fun logMessageWhenCondition(log: Logger, condition: Boolean) { 54 | if (condition) { 55 | log.info("a message") 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /logcapture-kotest/src/test/kotlin/org/logcapture/kotest/LogCaptureListenerWithCustomLoggerNameSpec.kt: -------------------------------------------------------------------------------- 1 | package org.logcapture.kotest 2 | 3 | import org.logcapture.assertion.ExpectedLoggingMessage.aLog 4 | import io.kotest.core.spec.style.StringSpec 5 | import org.hamcrest.Matchers.equalTo 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | 9 | class LogCaptureListenerWithCustomLoggerNameSpec : StringSpec({ 10 | 11 | val log: Logger = LoggerFactory.getLogger("CUSTOM-LOGGER") 12 | 13 | val logCaptureListener = LogCaptureListener("CUSTOM-LOGGER") 14 | listener(logCaptureListener) 15 | 16 | "verify log messages" { 17 | log.info("a message") 18 | 19 | logCaptureListener.logged(aLog().info().withLoggerName(equalTo("CUSTOM-LOGGER"))) 20 | logCaptureListener.logged(aLog().info().withMessage("a message")) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /logcapture-spock2/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | } 4 | 5 | dependencies { 6 | api project(":logcapture-core") 7 | 8 | implementation 'org.spockframework:spock-core:2.3-groovy-4.0' 9 | } 10 | 11 | test { 12 | useJUnitPlatform() 13 | } 14 | -------------------------------------------------------------------------------- /logcapture-spock2/src/main/groovy/org/logcapture/spock2/LogCaptureSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.logcapture.spock2 2 | 3 | import ch.qos.logback.classic.Logger 4 | import ch.qos.logback.classic.spi.ILoggingEvent 5 | import org.hamcrest.Matcher 6 | import org.logcapture.LogCapture 7 | import org.logcapture.logback.StubAppender 8 | import org.slf4j.LoggerFactory 9 | import spock.lang.Specification 10 | 11 | import static org.slf4j.Logger.ROOT_LOGGER_NAME 12 | 13 | class LogCaptureSpec extends Specification { 14 | 15 | def root 16 | def logAppender 17 | 18 | def setup() { 19 | logAppender = new StubAppender() 20 | root = (Logger) LoggerFactory.getLogger(ROOT_LOGGER_NAME) 21 | 22 | root.addAppender(logAppender) 23 | } 24 | 25 | def cleanup() { 26 | root.detachAppender(logAppender) 27 | } 28 | 29 | LogCapture logged(Matcher> expectedLoggingMessage) { 30 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage) 31 | } 32 | 33 | LogCapture logged(Matcher> expectedLoggingMessage, Integer times) { 34 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage, times) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /logcapture-spock2/src/main/groovy/org/logcapture/spock2/LogCaptureTrait.groovy: -------------------------------------------------------------------------------- 1 | package org.logcapture.spock2 2 | 3 | import ch.qos.logback.classic.Logger 4 | import ch.qos.logback.classic.spi.ILoggingEvent 5 | import org.hamcrest.Matcher 6 | import org.logcapture.LogCapture 7 | import org.logcapture.logback.StubAppender 8 | import org.slf4j.LoggerFactory 9 | 10 | trait LogCaptureTrait { 11 | 12 | def root 13 | def logAppender 14 | 15 | def setup() { 16 | logAppender = new StubAppender() 17 | root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) 18 | 19 | root.addAppender(logAppender) 20 | } 21 | 22 | def cleanup() { 23 | root.detachAppender(logAppender) 24 | } 25 | 26 | LogCapture logged(Matcher> expectedLoggingMessage) { 27 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage) 28 | } 29 | 30 | LogCapture logged(Matcher> expectedLoggingMessage, Integer times) { 31 | return new LogCapture<>(logAppender.events()).logged(expectedLoggingMessage, times) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /logcapture-spock2/src/test/groovy/org/logcapture/spock2/LogCaptureSpecShould.groovy: -------------------------------------------------------------------------------- 1 | package org.logcapture.spock2 2 | 3 | 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import spock.lang.Shared 7 | 8 | import static ch.qos.logback.classic.Level.DEBUG 9 | import static ch.qos.logback.classic.Level.INFO 10 | import static org.hamcrest.Matchers.* 11 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog 12 | 13 | class LogCaptureSpecShould extends LogCaptureSpec { 14 | 15 | @Shared 16 | Logger log = LoggerFactory.getLogger(getClass()) 17 | 18 | def "verify missing events"() { 19 | expect: 20 | logged(not(aLog() 21 | .withLevel(equalTo(INFO)) 22 | .withMessage(equalTo("missing message")))) 23 | } 24 | 25 | def "verify multiple events"() { 26 | expect: 27 | log.info("first message") 28 | log.debug("second message") 29 | logged allOf(aLog().withLevel(INFO).withMessage("first message"), 30 | aLog().withLevel(DEBUG).withMessage("second message")) 31 | } 32 | 33 | def "verify sync logs using rule"() { 34 | expect: 35 | log.info("a message") 36 | 37 | logged(aLog().info().withMessage("a message")) 38 | } 39 | 40 | def "verify n times logs"() { 41 | expect: 42 | log.info("a message") 43 | log.info("a message") 44 | 45 | logged(aLog().info().withMessage("a message"), 2) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /logcapture-spock2/src/test/groovy/org/logcapture/spock2/LogCaptureTraitShould.groovy: -------------------------------------------------------------------------------- 1 | package org.logcapture.spock2 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | import spock.lang.Shared 6 | import spock.lang.Specification 7 | 8 | import static ch.qos.logback.classic.Level.DEBUG 9 | import static ch.qos.logback.classic.Level.INFO 10 | import static org.hamcrest.Matchers.* 11 | import static org.logcapture.assertion.ExpectedLoggingMessage.aLog 12 | 13 | class LogCaptureTraitShould extends Specification implements LogCaptureTrait { 14 | 15 | @Shared 16 | Logger log = LoggerFactory.getLogger(LogCaptureTraitShould.class) 17 | 18 | def "verify missing events"() { 19 | expect: 20 | logged(not(aLog() 21 | .withLevel(equalTo(INFO)) 22 | .withMessage(equalTo("missing message")))) 23 | } 24 | 25 | def "verify multiple events"() { 26 | expect: 27 | log.info("first message") 28 | log.debug("second message") 29 | logged(allOf( 30 | aLog().withLevel(INFO).withMessage("first message"), 31 | aLog().withLevel(DEBUG).withMessage("second message")) 32 | ) 33 | } 34 | 35 | def "verify sync logs using rule"() { 36 | expect: 37 | log.info("a message") 38 | 39 | logged(aLog().info().withMessage("a message")) 40 | } 41 | 42 | def "verify n times logs"() { 43 | expect: 44 | log.info("a message") 45 | log.info("a message") 46 | 47 | logged(aLog().info().withMessage("a message"), 2) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradle.develocity") version "4.0.1" 3 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" 4 | } 5 | 6 | develocity { 7 | buildScan { 8 | termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") 9 | termsOfUseAgree.set("yes") 10 | } 11 | } 12 | 13 | 14 | include 'logcapture-core', 15 | 'logcapture-junit4', 16 | 'logcapture-junit5', 17 | 'logcapture-spock2', 18 | 'logcapture-kotest', 19 | 'logcapture-example' 20 | 21 | rootProject.name = 'logcapture' 22 | 23 | --------------------------------------------------------------------------------