QuickSeq stands for "Quick Sequence Diagrams" and is a plugin for Gradle and the Kotlin IR Compiler. It comes with a set of predefined annotations, which you can add to your most important methods. Then, during test runtime, the tool will automatically create logs of these methods and convert them to PlantUML sequence diagram syntax.
QuickSeq is based on
debuglog
by Brian Norman,
which again is based on debuglog
by Kevin Most.
Thanks for the amazing groundwork!
During compile time, QuickSeq transforms your annotated Kotlin methods so that they can generate special logs. Then, during test runtime, the methods actually write those logs.
When the tests have finished, the logs get translated to PlantUML syntax. You can view the diagrams with the PlantUML Plugin for IntelliJ.
The images were taken from this presentation; you can take a look for a better overview.
If you open javadoc/html/plugin/index.html in a browser (after you downloaded the code), you can read the documentation of QuickSeq's classes / methods / fields without having to use an IDE.
QuickSeq itself does not send any data anywhere, but we cannot guarantee this for all of the used plugins and libraries.
You can skip this step if the versions are compatible/equal.
- The versions in the plugin must be compatible with the versions in your "consumer" project – otherwise the plugin will probably not work
- The safest way is to use exactly the same versions as in your consumer project
- The current versions are:
- Kotlin: 1.4.30
- JVM: 11
- Gradle: 7.0.2
- You can do a full-text search to find the occurences
- First download and open the quickseq-annotations project and run
./gradlew publishToMavenLocal
- Then run the file
buildAndPublish.sh
in this project
- Create a directory named
libs
in your project (at the same file tree level as yourbuild.gradle.kts
) - If you skipped steps (0) and (1): Download the JARs from here
- Else: Go to your local Maven repository (usually
.m2
at your user's root) and then tocom/kiwi/quickseq
- Copy the following files to
libs
:quickseq-plugin/[VERSION]/quickseq-plugin-[VERSION].jar
quickseq-annotations/1.0.1/quickseq-annotations-1.0.1.jar
quickseq-annotations-jvm/1.0.1/quickseq-annotations-jvm-1.0.1.jar
Go to your consumer project's build.gradle.kts
and add the following code snippets in the given order:
If you don't have a buildscript
block yet, create one. Otherwise, add the following lines in the proper places:
buildscript {
repositories {
flatDir {
dirs("libs")
}
}
dependencies {
val quickseqVersion = "X.Y.Z" // Whatever version you want to use; see top-level build.gradle.kts of this project
val quickseqAnnotationVersion = "1.0.1"
classpath("com.kiwi.quickseq:quickseq-gradle:$quickseqVersion")
classpath("com.kiwi.quickseq:quickseq-annotations:$quickseqAnnotationVersion")
}
}
Add the flatDir
part to your repositories
block:
repositories {
flatDir {
dirs("libs")
}
}
Add this to your dependencies
block:
dependencies {
// You must use the same versions as above!
val quickseqVersion = "X.Y.Z"
val quickseqAnnotationVersion = "1.0.1"
implementation("com.kiwi.quickseq:quickseq-annotations:$quickseqAnnotationVersion")
implementation("com.kiwi.quickseq:plugin:$quickseqVersion")
}
The last part is pretty long, but luckily most IDEs allow to collapse code blocks. 😉 Add it to the root level of your build script, i.e. anywhere outside a block.
val diagramTask = com.kiwi.quickseq.QuickSeqDiagramTask(
projectRootDir = file(".").absolutePath,
generateUnfilteredDiagrams = true,
maxRowsPerDiagram = 100
)
val generateSequenceDiagrams by tasks.registering {
group = "documentation"
doLast { diagramTask.generate() }
}
val clearSequenceDiagrams by tasks.registering {
group = "documentation"
doLast { diagramTask.clearLogsAndDiagrams() }
}
if (project.hasProperty("useQuickSeq")) {
println("\nQuickSeq will be applied...\n")
apply(plugin = "com.kiwi.quickseq")
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
useIR = true
apply(plugin = "com.kiwi.quickseq")
}
}
// If you have more than one test task (e.g. test, integrationTest),
// make sure to add the "dependsOn" and "finalizedBy" to all of them!
tasks.test {
dependsOn(clearSequenceDiagrams)
finalizedBy(listOf(generateSequenceDiagrams))
}
}
There are two QuickSeq annotations:
-
@SequenceDiagram
is the "basic" annotation- You can put it on any method in your code (except tests) which you would like to see in your sequence diagrams
- The annotation has an option called
silentReturn
: If you are not interested in the function's return value and want less arrows in your diagram, you can configure the annotation to@SequenceDiagram(true)
. The default isfalse
.
-
@SequenceDiagramEntryPoint
- Only tests (
@Test
) can be annotated like this! - Per annotation, one diagram will be created – i.e. without this annotation, no diagram will be created
- You can add a diagram title like this:
@SequenceDiagramEntryPoint("My Title")
- Only tests (
If you want all methods of a certain class or file to be logged, you can also annotate your whole class or file at once:
@SequenceDiagram class Foo {...}
@file:SequenceDiagram
in the first line of your file (beforepackage
,import
etc.)
As you could see in some of the code snippets above, the plugin only gets applied if the project has the property "useQuickSeq".
if (project.hasProperty("useQuickSeq")) ...
You can add this property by adding -PuseQuickSeq
after your normal ./gradlew ... test
command. If you do not want to add this flag every time, I suggest you set up a run configuration in IntelliJ:
Here's an example:
(6) Download the PlantUML Plugin for IntelliJ
- If you have set up a run configuration, you can select and run it to create the diagrams.
- You must not run your tests in parallel!
- Doing so would confuse the logging mechanism and your diagrams would not make sense.
- A test run with QuickSeq does not replace a "normal" test run! (without QuickSeq)
- We cannot guarantee that your tests work 100% correct after they have been transformed. Actually, the body of tests which are not entry points get replaced with an empty body to run faster. Hence it means nothing if they pass.
- Always do a Gradle clean before and after you run QuickSeq!
- Otherwise Gradle might take the compilation results from the build cache
Every function needs to print an "exit log" in order to produce a return arrow in the diagram. Sometimes this is not possible though. Consider this example:
ClassB {
fun bar() {
return foobar()
}
}
It would be nice if the function did something like this:
ClassB {
fun bar() {
printEntryLog(...)
return foobar()
printExitLog(...)
}
}
...but the printExitLog(...)
statement would never be reached because it comes after the return. Instead, we're telling the function who called us that it should print our exit log as soon as we are done:
ClassB {
fun bar() {
printEntryLog(...)
printExitLog(deferLogging = true, ...)
return foobar()
}
}
In this specific case, ClassB@bar
tells ClassA@foo
: "Hey, I have been called by you, and I need you to print my exit log as soon as I'm done here".
Hence the transformed code of ClassA@foo
looks somewhat like this:
ClassA {
fun foo() {
printEntryLog(...)
ClassB().bar()
printDeferredLogIfNeeded() // Prints exit log of ClassB@bar()
printExitLog(...)
}
}
This diagram would result from the described scenario. The gray text shows the name of the current log context.
It would be nice to solve it as below, but for a yet unidentified reason, this resulted in a strange error. If you have an idea why this happens and how it can be solved, please leave a comment!
ClassB {
fun bar() {
printEntryLog(...)
val result = foobar()
printExitLog(...)
return result
}
}
The error: e: java.lang.IllegalStateException: org.jetbrains.kotlin.ir.declarations.impl.IrVariableImpl@6c2481da for VAR IR_TEMPORARY_VARIABLE name:tmp0 type:[...] has unexpected parent org.jetbrains.kotlin.ir.declarations.impl.IrFunctionImpl@5ce5be31