Code Documentation - Reports & JBake
Thomas J Kennedy
Make sure you have finished reading Code Documentation & Comments - Looking Back which covers code documentation (e.g., Javadoc comments).
1 There is More to Documentation…
Documentation includes more than just writing inline code comments and using Javadoc (Java), Pydoc (Python), Rustdoc (Rust), or another tool for API documentation. Documentation includes reports, e.g.,
- JUnit Test Results
- Unit Test Coverage
- Integration Test Coverage
- Combined Test Coverage
- Dependency Reports (e.g., libraries used to build a project)
- API Documentation
- Static Analysis Reports (e.g., PMD and Spotbugs)
A few of these tools (especially Jacoco, PMD, and Spotbugs) are covered in the next module (Analysis Tools).
2 A Case Study
Recently (during April 2020) I updated one of my CS 330 (Object Oriented Programming and Design) Java examples.
-
Grab a copy of Review-09-Java-Shapes.zip. Our case study will be Example-6.
-
Open a terminal and run
./gradlew bake
(or.\gradlew.bat bake
in Windows Command Prompt or Windows Powershell). -
Double click
documentation.htm
(a quick-and-dirty helper file) orbuild/jbake/home.html
. Your web browser will open. -
Take a few minutes to read through the home page and explore the left navigation bar.
Everything was generated using jbake and Gradle. A few items (e.g., Checkstyle and PMD) are covered in the next couple modules. For now, keep in mind that they exist, but do not dwell on them.
2.1 Project Structure
Take a quick look at the project structure. There are quite a few pieces. Take a few moments to look at the “big picture.”
Example 1: Java Shapes Directory Structure├── build ├── build.gradle ├── config │ ├── checkstyle │ │ └── checkstyle.xml │ └── documentation.config ├── documentation.htm ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── src ├── integrationTest │ └── java │ └── edu │ └── odu │ └── cs │ └── cs330 │ └── examples │ └── shapes │ ├── io │ │ └── TestShapeIterator.java │ └── TestShapeFactory.java ├── jbake │ ├── assets │ │ └── css │ │ └── base.css │ ├── content │ │ ├── checkstyle.md │ │ ├── coverageIntegration.md │ │ ├── coverage.md │ │ ├── coverageMerged.md │ │ ├── dependencies.md │ │ ├── home0.md │ │ ├── junitIntegration.md │ │ ├── junit.md │ │ ├── pmd.md │ │ ├── spotbugs.md │ │ └── tasks.md │ ├── jbake.properties │ └── templates │ ├── footer.ftl │ ├── header.ftl │ ├── index.ftl │ ├── menu.ftl │ ├── page.ftl │ ├── reportPage.ftl │ └── sitemap.ftl ├── main │ ├── java │ │ └── edu │ │ └── odu │ │ └── cs │ │ ├── cs330 │ │ │ └── examples │ │ │ ├── package-info.java │ │ │ ├── RunShapes.java │ │ │ └── shapes │ │ │ ├── Circle.java │ │ │ ├── EquilateralTriangle.java │ │ │ ├── io │ │ │ │ ├── package-info.java │ │ │ │ └── ShapeIterator.java │ │ │ ├── package-info.java │ │ │ ├── RightTriangle.java │ │ │ ├── ShapeFactory.java │ │ │ ├── Shape.java │ │ │ ├── Square.java │ │ │ └── Triangle.java │ │ └── tkennedy │ │ └── utilities │ │ ├── package-info.java │ │ └── Utilities.java │ └── resources │ └── inputShapes.txt └── test └── java └── edu └── odu └── cs └── cs330 └── examples └── shapes ├── TestCircle.java ├── TestEquilateralTriangle.java ├── TestRightTriangle.java ├── TestSquare.java └── TestTriangle.java
You should recognize a few items from Gradle Whirlwind Introduction:
-
build.gradle
- This is the Gradle build file. It specifies which tasks to run, which plugins to use, and a few settings. -
config
- This is an auxiliary directory I use to store configuration files (e.g., checkstyle and other code linting rules). -
gradle
- This is a directory containing Gradle Wrapper files. These files allow us to run Gradle without needing to intall it first. -
gradle.properties
- This is an optional file that can be used to specify additional settings. -
gradlew
- This is a bootstrap/wrapper script for Linux and macOS. -
gradlew.bat
- This is a bootstrap/wrapper script for Windows. -
makefile
- Ignore this makefile. It is little more than a wrapper around Gradle. -
src
- This directory contains allmain
andtest
code.
Let us focus on the first two levels.
Example 2: Directory Structure: First Two Levels├── build ├── build.gradle ├── config │ ├── checkstyle │ │ └── checkstyle.xml │ └── documentation.config ├── documentation.htm ├── gradle ├── gradle.properties ├── gradlew ├── gradlew.bat └── src ├── integrationTest ├── jbake ├── main └── test
Let us focus on 5 items:
-
config/checkstyle/checkstyle.xml
- this is the configuration file for checkstyle. -
src/main
- this directory contains the main (production) code and resources. -
src/test
- this directory contains all unit test code. -
src/integrationTest
- this directory contains all integration test code. -
src/jbake
- this directory contains all templates, settings, and content for JBake.
2.2 JBake Directory Structure
We are interested in src/jbake
.
Example 3: JBake src Files└── src ├── jbake │ ├── assets │ │ └── css │ │ └── base.css │ ├── content │ │ ├── checkstyle.md │ │ ├── coverageIntegration.md │ │ ├── coverage.md │ │ ├── coverageMerged.md │ │ ├── dependencies.md │ │ ├── home0.md │ │ ├── junitIntegration.md │ │ ├── junit.md │ │ ├── pmd.md │ │ ├── spotbugs.md │ │ └── tasks.md │ ├── jbake.properties │ └── templates │ ├── footer.ftl │ ├── header.ftl │ ├── index.ftl │ ├── menu.ftl │ ├── page.ftl │ ├── reportPage.ftl │ └── sitemap.ftl
JBake has a few pieces:
-
src/jbake/assets/css
any CSS files that are used for formatting. In this configuration, I have a few manual CSS rules set and rely on Bootstrap for most of the CSS layout/formatting. -
src/jbake/content
- all page content to display, including metadata. I prefer markdown (.md
files) for page content. -
src/jbake/templates
- collection of layouts. There is one file per page. A few files (e.g.,menu.ftl
) are known as partials. Think of partials as#include
orimport
statements. Partials are used to share common pieces of layouts between pages. -
src/jbake/jbake.properties
- general JBake settings (e.g., pages to generate).
2.2.1 jbake.propertes
JBake uses the ...
before and after the settings to mark the beginning and end. The ...
must be present.
...
render.index=true
render.archive=false
render.feed=false
render.tags=false
render.sitemap=false
template.reportPage.file=reportPage.ftl
markdown.extensions=-HARDWRAPS
...
The render.
settings are built-in to JBake. Each such settings is set to true
to enable generation of a page or false
to disable generation of a page.
The template.reportPage
setting specifies which layout file corresponds to my custom report page template.
The markdown.extensions
setting allows markdown settings to be changes. In the case -=HARDWRAPS
tells JBake to ignore any hardcoded line breaks in content files.
2.2.2 JBake Templates
In software engineering there exist a few mantras. One is D.R.Y (Don’t repeat yourself). Each report page (e.g., checkstyle.md
) shares the same basic structure. After a little trial and error, I came up with the current version of reportPage.ftl
.
Example 4: reportPage.ftl
<#include "header.ftl"> <div class="row h-100"> <div class="col-sm-3 col-md-2 bg-light hidden-md-down" id="main-nav"> <#include "menu.ftl"> </div> <div class="col"> <div class="mh-90"> <div class="pageHeader"> <h1><#escape x as x?xml>${content.title}</#escape></h1> </div> <p><em>${content.date?string("dd MMMM yyyy")}</em></p> ${content.body} <div class="embed-responsive ${content.report_iframe_aspectratio}"> <iframe class="embed-responsive-item" src="${content.report_file}" allowfullscreen> </iframe> </div> </div> </div> </div> <#include "footer.ftl">
Most of the CSS classes (e.g., col-sm-3
) come from Bootstrap helpers. Pay particular attention to pieces in the form ${...}
. These are variables. They are replaced based on metadata located in a content file. Consider src/content/junit.md
.
Example 5: Content Pagetitle=Java Shapes Example - Unit Tests type=reportPage status=published report_file=tests/test/index.html report_iframe_aspectratio=embed-responsive-4by3 ~~~~~~
In this case, junit.md
only contains metadata. I could add additional content under the ~~~~~~
(yes, the number of tilda
s is important).
title
- page title.type
- type of page to generate (i.e., which template to use).status
- set to published for every “active” page.report_file
- which HTML report file (e.g., JUnit) to embed in the generated page.report_iframe_aspectratio
- the bootstrap CSS class to use for the embedded report (determines the aspect ratio of the embedded report).
2.3 Gradle Updates
So far… we have only looked at the JBake directory and file structure. How do we update build.gradle
? Take a quick look at the complete build.gradle
. There is quite a bit going on, including:
- test configuration
- analysis tool configuration
- custom Gradle tasks
buildscript {
repositories {
jcenter()
}
}
plugins {
id "java"
id "application"
id "eclipse"
id "checkstyle"
id "com.github.spotbugs" version "2.0.0"
id "project-report"
id "jacoco"
id "pmd"
id "org.ysb33r.doxygen" version "0.5"
id "org.jbake.site" version "5.0.0"
// Split Integration Tests from Unit Tests
id "org.unbroken-dome.test-sets" version "3.0.1"
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
jcenter()
}
dependencies {
testImplementation "junit:junit:4.12"
testImplementation "org.hamcrest:hamcrest-library:1.3"
}
jar {
archiveBaseName = "RunShapes"
manifest {
attributes(
"Main-Class": "edu.odu.cs.cs330.examples.RunShapes"
)
}
}
run {
main = "edu.odu.cs.cs330.examples.RunShapes"
args = ["src/main/resources/inputShapes.txt", "2"]
}
application {
mainClassName = "edu.odu.cs.cs330.examples.RunShapes"
}
test {
reports {
html.enabled = true
}
ignoreFailures = true
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
// Set up Integration Tests
testSets {
integrationTest
}
check.dependsOn integrationTest
//------------------------------------------------------------------------------
// Documentation Tools
//------------------------------------------------------------------------------
javadoc {
failOnError false
}
doxygen {
generate_html true
template "config/documentation.config"
source new File(projectDir, "src/main/java")
//source new File(projectDir, "src/test/java")
outputDir new File(buildDir, "doxygen")
}
//------------------------------------------------------------------------------
// Analysis Tools
//------------------------------------------------------------------------------
// SpotBugs
spotbugsMain {
ignoreFailures = true
effort = "max"
reportLevel = "medium"
reports {
xml.enabled = false
html.enabled = true
}
}
spotbugsTest.enabled = false
spotbugsIntegrationTest.enabled = false
// End SpotBugs config
jacoco {
toolVersion = "0.8.5"
}
jacocoTestReport {
reports {
html.enabled true
xml.enabled true
csv.enabled true
}
}
task mergeCoverageReports(type: JacocoMerge) {
executionData = layout.files(["build/jacoco/test.exec",
"build/jacoco/integrationTest.exec"])
destinationFile = "build/jacoco/merged.exec" as File
}
/*
* This task is based on HenrikBaerbak's example at
* <https://discuss.gradle.org/t/merge-jacoco-coverage-reports-for-multiproject-setups/12100/10>
*/
task mergedJacocoReportHTML (type: JacocoReport) {
dependsOn(mergeCoverageReports)
additionalSourceDirs.from = files(sourceSets.main.allSource.srcDirs)
sourceDirectories.from = files(sourceSets.main.allSource.srcDirs)
classDirectories.from = files(sourceSets.main.output)
executionData.from = files("build/jacoco/merged.exec" as File)
reports {
xml.enabled false
csv.enabled false
html.enabled true
}
}
// Check Style Config
checkstyle {
toolVersion "8.2"
ignoreFailures = true
showViolations = false
}
tasks.withType(Checkstyle) {
reports {
html.destination project.file("build/reports/checkstyle/main.html")
}
}
checkstyleTest.enabled = false
checkstyleIntegrationTest.enabled = false
// End Checkstyle config
pmd {
toolVersion = "6.21.0"
ignoreFailures = true
ruleSets = [
"category/java/bestpractices.xml",
"category/java/codestyle.xml",
"category/java/design.xml",
"category/java/errorprone.xml",
"category/java/performance.xml"
]
}
pmdTest.enabled=false
pmdIntegrationTest.enabled=false
//------------------------------------------------------------------------------
// JBake Configuration
//------------------------------------------------------------------------------
task reports (dependsOn: ["javadoc", "doxygen",
"check",
"jacocoTestReport",
"jacocoIntegrationTestReport",
"mergedJacocoReportHTML",
"projectReport"]) {
description "Generate all reports and documentation for this project"
}
task copyJDocs (type: Copy) {
from "build/docs"
into "build/tmp/website/assets"
dependsOn "javadoc"
}
task copyDoxygen (type: Copy) {
from "build/doxygen"
into "build/tmp/website/assets/doxygen"
dependsOn "doxygen"
}
task copyReports (type: Copy) {
from "build/reports"
into "build/tmp/website/assets"
dependsOn "reports"
}
task copyJbakeTemplates (type: Copy) {
from "src/jbake"
into "build/tmp/website"
}
// Combine home0.md and the project README.md into a single homepage
task buildHomePage (dependsOn: copyJbakeTemplates) {
inputs.files ( "build/tmp/website/content/home0.md", "../README.md")
outputs.file ("build/tmp/website/content/home.md")
doLast {
outputs.files.singleFile.withOutputStream { out ->
for (file in inputs.files) file.withInputStream {
out << it << '\n'
}
}
}
}
jbake {
srcDirName = "build/tmp/website"
}
// Ensure all Copy and JBake build tasks run
task setupWebsite (dependsOn: ["buildHomePage", "copyReports", "copyJDocs", "copyDoxygen"]){
}
bake.dependsOn "setupWebsite"
Let us focus on the Gradle plugins block and everything below
//------------------------------------------------------------------------------
// JBake Configuration
//------------------------------------------------------------------------------
The rest of build.gradle
deals with tool configuration (which is a future discussion topic).
buildscript {
repositories {
jcenter()
}
}
plugins {
id "java"
id "application"
id "eclipse"
id "checkstyle"
id "com.github.spotbugs" version "2.0.0"
id "project-report"
id "jacoco"
id "pmd"
id "org.ysb33r.doxygen" version "0.5"
id "org.jbake.site" version "5.0.0"
// Split Integration Tests from Unit Tests
id "org.unbroken-dome.test-sets" version "3.0.1"
}
Most of the plugin blocks deal with analysis tools, testing, or general project setup. We are interested in one line in particular… the line that adds the JBake plugin.
id "org.jbake.site" version "5.0.0"
The first task (i.e., reports) is for convenience.
task reports (dependsOn: ["javadoc", "doxygen",
"check",
"jacocoTestReport",
"jacocoIntegrationTestReport",
"mergedJacocoReportHTML",
"projectReport"]) {
description "Generate all reports and documentation for this project"
}
Defining reports
allows us to use ./gradlew reports
to generate every report. The reports
task triggers everything listed after dependsOn:
.
The next few tasks copy each report into a temporary website build directory.
task copyJDocs (type: Copy) {
from "build/docs"
into "build/tmp/website/assets"
dependsOn "javadoc"
}
task copyDoxygen (type: Copy) {
from "build/doxygen"
into "build/tmp/website/assets/doxygen"
dependsOn "doxygen"
}
task copyReports (type: Copy) {
from "build/reports"
into "build/tmp/website/assets"
dependsOn "reports"
}
The copyJBakeTemplates
task copies everything from src/jbake
into the temporary website build directory.
task copyJbakeTemplates (type: Copy) {
from "src/jbake"
into "build/tmp/website"
}
The buildHomePage
task combines home0.md
with the project README.md
. Note that you will probably need to change ../README.md
to ./README.md
in your own Gradle projects.
// Combine home0.md and the project README.md into a single homepage
task buildHomePage (dependsOn: copyJbakeTemplates) {
inputs.files ( "build/tmp/website/content/home0.md", "../README.md")
outputs.file ("build/tmp/website/content/home.md")
doLast {
outputs.files.singleFile.withOutputStream { out ->
for (file in inputs.files) file.withInputStream {
out << it << '\n'
}
}
}
}
The next task tells JBake to use the build/tmp/website
directory during the build.
jbake {
srcDirName = "build/tmp/website"
}
The setUpWebsite
task is another helper task. It ensures that all the preliminary setup steps happen before the bake
task runs.
// Ensure all Copy and JBake build tasks run
task setupWebsite (dependsOn: ["buildHomePage", "copyReports", "copyJDocs", "copyDoxygen"]){
}
The last line guarantees that setupWebsite
runs before the bake task
.
bake.dependsOn "setupWebsite"
The final step is to… run ./gradlew bake
wait for the result!