Project Reports & Websites
Steven J Zeil
Abstract
In this lesson we will continue looking at project documentation:
- Project reports
- Generating project websites
We will look at how to use our automated build tools to keep project reporting up-to-date.
1 Project Reports
1.1 Test Reports
We’ve already looked at JUnit, which can be used to generate test reports like this one or this one.
This is generated in Gradle via the java
plugin. It produces an XML summary and a formatted reports in build\reports
.
1.2 Static Code Analyzers
Many tools that we will cover later for analyzing code can produce useful (or at least, impressive) documentation as a side effect.
1.3 Configuration Reports
Configuration managers generate reports about the dependencies among the software components.
For example, this report comes from Gradle by adding
plugins {
⋮
id 'project-report'
⋮
}
to the build.xml
file
2 Project Websites
-
In general, websites can be “manually” constructed, file by file, or any of a number of site-builder tools can be employed to create large numbers of pages that share a common look-and-feel.
-
An innovation of Maven was to consider construction of project websites as a part of the automated build.
-
In Gradle this can be managed by custom tasks or by various plugins.
2.1 Case study: Constructing a project website with Gradle
Let’s look at the process of building a simple website that provides
- A brief project description
- Links to (and copies of)
- the project configuration report
- the JUnit test report
- the Javadoc API documentation
We can consider adding other content in later lessons.
2.1.1 Build plan
After a build, we can construct a website in build/reports
by
-
Copying
build/doc/
intobuild/website
-
This gets a copy of the Javadocs
-
Note that the JUnit and project configuration reports will already be in subdirectories of
build/reports
.
-
-
Copying HTML, CSS, and Javascript files from
src/main/html
into reports.-
Hand-crafted pages with the project description and links to the three reports.
-
2.1.2 The HTML File
<!DOCTYPE html>
<html>
<head>
<title>Project Reports</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="projectReports.css"/>
<script src="projectReports.js"></script>
</head>
<body>
<h1>CodeCompCommon</h1>
<p>This project provides a common set of interfaces for all CS350
CodeComp project variants.</p>
<p>It also provides a SharedPhrases class used to store token lists
obtained via lexical analysis of the source code.</p>
<h2>Information</h2>
<p>Current version: 1.3</p>
<h2>Detailed Reports</h2>
<div class="reportLinks">
<ul>
<li>General:
<ul>
<li><a href="../docs/javadoc/">JavaDoc</a></li>
<li><a href="../project/dependencies/root.html">Project Dependencies</a></li>
</ul></li>
<li>Tests:
<ul>
<li><a href="../tests/test/">JUnit tests</a></li>
</ul>
</li>
</ul>
</div>
</body>
</html>
2.1.3 Gradle setup
After a build, we can construct a website in build/reports
by
- Copying
build/doc/
intobuild/website
- Copying HTML, CSS, and Javascript files from
src/main/html
into reports.test.ignoreFailures=true ➀ check.dependsOn htmlDependencyReport ➁ task copyWebPages (type: Copy) { ➄ from 'src/main/html' into 'build/reports' } task copyDocs (type: Copy) { ➃ from 'build/docs' into 'build/reports/docs' dependsOn 'javadoc' } task buildSite (dependsOn: ['javadoc', 'check', ➂ 'copyWebPages', 'copyDocs']) { description 'Generate reports website for this project' }
-
➀ Don’t want the build to stop just because some unit tests are failing
- In TDD, some tests are almost always failing
- One reason for posting the JUnit report is so that people can see what we fail and what we pass.
-
➁ Force the generation of the configuration report whenever we build the
check
target/task. -
➂ Here’s our new target,
buildSite
.It has no actions of its own, but the depedencies force everything else to happen.
-
➃ Here’s step 1 of our process: copying the
docs
into the website - ➄ Here’s step 2 of our process: copying the hand-crafted content from
src/html
into the website
2.2 Case study: Constructing a project website with Gradle and Jbake
For a more elaborate, multi-page site, we can use a website generator to make it easy to enforce a common look-and-feel across pages.
Jbake injects content uses a series of templates
-
Content files can contain HTML, or Markdown, or a number of other text forms.
-
Each has a small amount of metadata at the top. Among other things, this identifies the starting template used to render that content.
-
-
Templates are HTML-like snippets that indicate where to inject content and what other templates to include.
A typical “page” template may load separate templates for headers, footers, navigation bars, menus, etc.
2.2.1 Gradle & Jbake
Gradle has a jbake
plugin that adds a bake
task to generate a website.
- By default looks for source files in
src/jbake
- By default writes the site into
build/jbake
buildscript { ⋮ dependencies { ⋮ classpath "org.jbake:jbake-gradle-plugin:5.0.0" } } ⋮ apply plugin: 'org.jbake.site'
2.2.2 The website setup
src
├── jbake
│ ├── assets
│ │ ├── css
│ │ │ └── base.css
│ │ └── js
│ │ └── jquery-1.11.1.min.js
│ ├── content
│ │ ├── dependencies.html
│ │ ├── home0.md
│ │ ├── javadoc.html
│ │ └── junit.html
│ ├── jbake.properties
│ └── templates
│ ├── footer.ftl
│ ├── header.ftl
│ ├── index.ftl
│ ├── menu.ftl
│ ├── page.ftl
│ └── sitemap.ftl
The content
directory` holds the actual information we want to disseminate.
The templates
directory determines the “look and feel” of the website.
- The key template is
page.ftl
- Every content file will be rendered using this page tempalte.
Templates:
<#include "header.ftl">
<div class="page-header">
<h1><#escape x as x?xml>${content.title}</#escape></h1>
</div>
<p><em>${content.date?string("dd MMMM yyyy")}</em></p>
<div class="center">
<#include "menu.ftl">
<div class="rightpart">
<p>${content.body}</p>
</div>
</div>
<hr />
<#include "footer.ftl">
- The content is loaded as
${content.body}
. - The page tempalte also
#include
s the header, menu, and footer templates.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title><#if (content.title)??><#escape x as x?xml>${content.title}</#escape><#else>codecentric</#if></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<meta name="keywords" content="">
<meta name="generator" content="JBake">
<link href="<#if (content.rootpath)??>${content.rootpath}<#else></#if>css/base.css" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="<#if (content.rootpath)??>${content.rootpath}<#else></#if>js/html5shiv.min.js"></script>
<![endif]-->
<link rel="shortcut icon" href="<#if (content.rootpath)??>${content.rootpath}<#else></#if>favicon.ico">
</head>
<body>
<div id="mainBody">
<div class="leftPart">
<div class="menuBlock">
<span class="menuBlockHeader"><a href="<#if
(content.rootpath)??>${content.rootpath}<#else></#if>home.html">Home</a></span>
</div>
<div class="menuBlock">
<span class="menuBlockHeader">Project Info</span>
<ul>
<li><a href="<#if (content.rootpath)??>${content.rootpath}<#else></#if>javadoc.html">API (Javadoc)</a></li>
<li><a href="<#if (content.rootpath)??>${content.rootpath}<#else></#if>dependencies.html">Dependencies</a></li>
</ul>
</div>
<div class="menuBlock">
<span class="menuBlockHeader">Testing</span>
<ul>
<li><a href="<#if
(content.rootpath)??>${content.rootpath}<#else></#if>junit.html">Unit
Tests</a></li>
<!--
<li><a href="<#if
(content.rootpath)??>${content.rootpath}<#else></#if>jacoco.html">Coverage</a></li>
-->
</ul>
</div>
<!--
<div class="menuBlock">
<span class="menuBlockHeader">Analysis Reports</span>
<ul>
<li><a href="<#if (content.rootpath)??>${content.rootpath}<#else></#if>checkstyle.html">Checkstyle</a></li>
<li><a href="<#if (content.rootpath)??>${content.rootpath}<#else></#if>dependencies.html">Dependencies</a></li>
</ul>
</div>
-->
</div>
</div>
<div id="footer">
<div class="container">
<p class="muted credit">© 2019 Old Dominion University</p>
</div>
</div>
<script src="<#if (content.rootpath)??>${content.rootpath}<#else></#if>js/jquery-1.11.1.min.js"></script>
</body>
</html>
Content:
title=CodeCompCommon Unit Tests
type=page
status=published
~~~~~~
</p>
<iframe class="docFrame" src="tests/test/index.html"/>
<p>
title=Project Documentation: CodeCompCommon
date=2019-03-18
type=page
status=published
~~~~~~
2.2.3 Build Steps
To generate a website in build/jbake
- Copy the
build/reports
andbuild/docs
tobuild/tmp/website
- Copy
src/jbake
tobuild/tmp/website
- Concatenate
build/tmp/website/content/home0.md
and the projectREADME.md
to formbuild/tmp/website/content/home.md
- Run
Jbake
onbuild/tmp/website/content/home.md
to generate the full website inbuild/jbake
In build.gradle
:
test.ignoreFailures=true
check.dependsOn htmlDependencyReport
task reports (dependsOn: ['javadoc', 'check']) {
description 'Generate all reports for this project'
}
task copyJDocs (type: Copy) {
from 'build/docs'
into 'build/tmp/website/assets'
dependsOn 'javadoc'
}
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'
}
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"
}
task setupWebsite (dependsOn: ['buildHomePage', 'copyReports', 'copyJDocs']){
}
bake.dependsOn "setupWebsite"
- New target is
bake
which runsJBake
and depends onsetupWebsite
setupWebsite
carries out most of our process- Trickiest part is
buildHomePage
, which carries out the concatentation - Most of the rest resembles our previous website build
- Trickiest part is
2.3 Publishing to a website
The case studies above will build a website, but we still need to upload that to a web server.
- Problems are not unlike publishing libraries to repositories.
2.3.1 Gradle: ssh solution
Gradle plugin org.hidetake:gradle-ssh-plugin
allows you to
- Use
scp
to upload or download one file at a time from/to a remote server. - Use
ssh
to issue commands to a remote server.
A plausible set of Gradle steps:
- Create a
.zip
file of the entire constructed website - Use
scp
to upload the zip file to the remote server. - Use
ssh
to issue anunzip
command on the remote server. - If necessary, use
ssh
to issuechmod
commands as necessary on the unzipped content.
In build.gradle
plugins {
id 'org.hidetake.ssh' version '2.9.0'
}
task zipWebsite (type: Zip, dependsOn: 'bake') { ➀
archiveFileName = 'website.zip'
destinationDirectory = file('build')
from 'build/jbake'
}
remotes {
webServer {
host = IP address
user = userName
identity = file(ssh-private-key) ➁
}
}
task deploy (dependsOn: 'zipWebsite') {
doLast {
ssh.run {
session(remotes.webServer) {
put from: 'build/website.zip', into: 'websitePath' ➂
execute 'unzip websitePath/website.zip' -d websitePath➃
}
}
}
}
- ➀ zip up the website (assuming our earlier Jbake example)
- ➁ assumes this key is in a running key agent (or passphrase-free)
- ➂ Copy the zip file to the remote host
- ➃ Tell the remote host to unpack the zip file
2.3.2 Gradle: rsync solution
This approach can be used with the rsync
-based file drops set up in earlier labs & assignments.
The Java library rsync4j-all
provides a Java interface to rsync
:
- On Linux/MacOS machines, assumes that a native
rsync
command is present. - On Windows machines, provides a minimal version of the CygWin environment with
ssh
andrsync
executables.
In build.gradle
buildscript {
/*...*.
dependencies {
⋮
classpath "com.github.fracpete:rsync4j-all:3.1.2-15"
}
}
import com.github.fracpete.rsync4j.RSync; ➀
import com.github.fracpete.processoutput4j.output.ConsoleOutputProcessOutput;
task deployWebsite (dependsOn: "bake") {
doLast {
def sourceDir = "build/jbake/";
def destURL = "destination"; ➁
RSync rsync = new RSync()
.source(sourceDir)
.destination(destURL)
.recursive(true)
.archive(true)
.delete(true)
.verbose(true)
.rsh("ssh -o IdentitiesOnly=yes"); ➂
ConsoleOutputProcessOutput output
= new ConsoleOutputProcessOutput();
output.monitor(rsync.builder());
}
}
- ➀ The
rSync4j
library, loaded in the dependencies section above, is not a plugin. It’s jsut a library. But we can use Java libraries in Gradle (Groovy) pretty much like we use them in Java. - ➁ Give the
ssh
URL for the website destination here. - ➂ assumes that we have a suitable ssh key in a running key agent
2.4 Forges
A software forge is a collection of web services for the support of collaborative software devlopment:
-
Project web sites
-
Networked access to version control
- Release (download) support
-
Communications (e.g., messaging, wikis, announcements)
-
Bug reporting and tracking
-
Project personnel management
Forge Examples
Among the best known forges are
-
the original, SourceForge, (1999)
-
Google Code, (2006)
-
GitHub, (2008)