Skip to content

Commit

Permalink
Avoid NPE in config initialization
Browse files Browse the repository at this point in the history
and provide strong test coverage for Gradle task/plugin
  • Loading branch information
ascheman committed Sep 18, 2024
1 parent f60b64a commit 2ef70b3
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import org.gradle.api.tasks.TaskAction
*/
class HtmlSanityCheckTask extends DefaultTask {

//
// we support checking several named files
@InputFiles
FileCollection sourceDocuments
Expand Down Expand Up @@ -80,28 +79,22 @@ class HtmlSanityCheckTask extends DefaultTask {
// private stuff
// **************************************************************************

private Set<File> allFilesToCheck

private Configuration myConfig


/**
* Sets sensible defaults for important attributes.
*
* Ensures that task is _run-always_,
* by setting outputs.upToDateWhen to false.
*/
HtmlSanityCheckTask() {

// Never consider this task up-to-date.
// thx https://github.com/stevesaliman/gradle-cobertura-plugin/commit/d61191f7d5f4e8e89abcd5f3839a210985526648
outputs.upToDateWhen { false }

// give sensible default for output directory, see https://github.com/aim42/htmlSanityCheck/issues/205
checkingResultsDir = new File(project.buildDir, '/reports/htmlSanityCheck/')
junitResultsDir = new File(project.buildDir, '/test-results/htmlSanityCheck/')


checkingResultsDir = new File(project.DEFAULT_BUILD_DIR_NAME, '/reports/htmlSanityCheck/')
junitResultsDir = new File(project.DEFAULT_BUILD_DIR_NAME, '/test-results/htmlSanityCheck/')
}

void setSourceDir(File sourceDir) {
Expand All @@ -118,7 +111,6 @@ class HtmlSanityCheckTask extends DefaultTask {
*/
@TaskAction
void sanityCheckHtml() {

// tell us about these parameters
logBuildParameter()

Expand All @@ -138,10 +130,9 @@ class HtmlSanityCheckTask extends DefaultTask {
assert junitResultsDir.canWrite()
}

// TODO: unclear: do we need to adjust pathnames if running on Windows(tm)??
// TODO: unclear: do we need to adjust path-names if running on Windows(tm)??

logger.info("buildfile-info", sourceDocuments?.toString())
logger.info("allFilesToCheck" + allFilesToCheck.toString(), "")
logger.info("Source documents: '{}'", sourceDocuments)

// create an AllChecksRunner...
def allChecksRunner = new AllChecksRunner(myConfig)
Expand All @@ -160,14 +151,13 @@ See ${checkingResultsDir} for a detailed report."""
throw new GradleException(failureMsg)
}
} else {
logger.warn("""Fatal configuration errors preventing checks:\n
${myConfig.toString()}""")
logger.error("Fatal configuration errors preventing checks:\n{}", myConfig)
}
}

/**
* setup a @Configuration instance containing all given configuration parameters
* from the gradle buildfile.
* from the gradle build-file.
*
* This method has to be updated in case of new configuration parameters!!
*
Expand All @@ -177,7 +167,7 @@ See ${checkingResultsDir} for a detailed report."""
protected Configuration setupConfiguration() {

Configuration result = Configuration.builder()
.sourceDocuments(sourceDocuments.files)
.sourceDocuments(sourceDocuments?.files)
.sourceDir(sourceDir)
.checkingResultsDir(checkingResultsDir)
.junitResultsDir(junitResultsDir)
Expand Down Expand Up @@ -210,16 +200,13 @@ See ${checkingResultsDir} for a detailed report."""

private void logBuildParameter() {
logger.info "=" * 70
logger.info "Parameters given to sanityCheck plugin from gradle buildfile..."
logger.info "Parameters given to sanityCheck plugin from gradle build-file..."
logger.info "Files to check : $sourceDocuments"
logger.info "Source directory: $sourceDir"
logger.info "Results dir : $checkingResultsDir"
logger.info "JUnit dir : $junitResultsDir"
logger.info "Fail on errors : $failOnErrors"

}


}

/*========================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.aim42.htmlsanitycheck

import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification

class HtmlSanityCheckBaseSpec extends Specification {
final static VALID_HTML = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"><html><head></head><body></body><html>"""
final static INVALID_HTML = """<body><span id="id"/><span id="id"/></body> """

@Rule
TemporaryFolder testProjectDir = new TemporaryFolder()
File sourceDir
File buildDir
File buildFile
File htmlFile

def setup() {
buildDir = testProjectDir.newFolder("build")
sourceDir = testProjectDir.newFolder("src")
sourceDir.mkdirs()
htmlFile = new File (sourceDir, "test.html")
}

protected void createBuildFile(String extendedTaskConfig = "") {
// a note on writing paths to the build script on windows:
// - the default file separator is a backslash
// - as the path is written into a quoted string, backslashes should be quoted
// - to avoid string manipulation or similar, we use URIs to avoid the problem
// (URIs consist of / instead of backslashes)
buildFile = testProjectDir.newFile('build.gradle') << """
plugins {
id 'org.aim42.htmlSanityCheck'
}
htmlSanityCheck {
sourceDir = file ("src")
checkingResultsDir = file ("build")
${extendedTaskConfig}
}
""".stripIndent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.aim42.htmlsanitycheck

import org.gradle.api.Project
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.testfixtures.ProjectBuilder
import spock.lang.Specification

class HtmlSanityCheckPluginSpec extends Specification {

def "plugin adds an htmlSanityCheck task to the project"() {
given: "a project"
Project project = ProjectBuilder.builder().build()

when: "the plugin is applied"
project.plugins.apply HtmlSanityCheckPlugin

then: "the htmlSanityCheck task is added"
project.tasks.named(HtmlSanityCheckPlugin.HTML_SANITY_CHECK) != null
}

def "htmlSanityCheck task has correct type and properties"() {
given: "a project with the plugin applied"
Project project = ProjectBuilder.builder().build()
project.plugins.apply(HtmlSanityCheckPlugin)

when: "retrieving the htmlSanityCheck task"
def task = project.tasks.named(HtmlSanityCheckPlugin.HTML_SANITY_CHECK).get()

then: "the task is of type HtmlSanityCheckTask"
task instanceof HtmlSanityCheckTask

and: "the task has the correct description"
task.description == 'performs semantic checks on html files'

and: "the task is in the verification group"
task.group == LifecycleBasePlugin.VERIFICATION_GROUP
}
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,29 @@
package org.aim42.htmlsanitycheck

import org.gradle.api.GradleException
import org.gradle.testkit.runner.GradleRunner
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
import spock.lang.Unroll

import static org.gradle.testkit.runner.TaskOutcome.FAILED
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS

class HtmlSanityCheckTaskFunctionalTest extends Specification {
private final static VALID_HTML = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"><html><head></head><body></body><html>"""
private final static INVALID_HTML = """<body><span id="id"/><span id="id"/></body> """
class HtmlSanityCheckTaskFunctionalSpec extends HtmlSanityCheckBaseSpec {
private final static GRADLE_VERSIONS = [ // 6.x or older does not work!
'7.6.3', // latest 7.x
'8.0.2', '8.1.1', '8.2.1', '8.3', '8.4',
'7.6.3', // latest 7.x
'8.0.2', '8.1.1', '8.2.1', '8.3', '8.4',
'8.5', '8.6', '8.7', '8.8', '8.9',
'8.10', '8.10.1' // all 8.x (latest patches)
]

@Rule
TemporaryFolder testProjectDir = new TemporaryFolder()
File buildDir
File buildFile
File htmlFile

def setup() {
buildDir = testProjectDir.newFolder("build")
htmlFile = testProjectDir.newFile("test.html")
// a note on writing paths to the build script on windows:
// - the default file separator is a backslash
// - as the path is written into a quoted string, backslashes should be quoted
// - to avoid string manipulation or similar, we use URIs to avoid the problem
// (URIs consist of / instead of backslashes)
buildFile = testProjectDir.newFile('build.gradle') << """
plugins {
id 'org.aim42.htmlSanityCheck'
}
htmlSanityCheck {
sourceDir = file( "${htmlFile.parentFile.toURI().path}" )
checkingResultsDir = file( "${buildDir.toURI().path}" )
}
"""
}
'8.10.1' // all 8.x (latest patches)
]

@Unroll
def "can execute htmlSanityCheck task with Gradle version #gradleVersion"() {
given:
htmlFile << VALID_HTML
createBuildFile()

when:

def result = runnerForHtmlSanityCheckTask(gradleVersion).build()

then:
Expand All @@ -67,11 +37,9 @@ class HtmlSanityCheckTaskFunctionalTest extends Specification {
def "invalid HTML fails build with failOnErrors=true and Gradle version #gradleVersion"() {
given:
htmlFile << INVALID_HTML
buildFile << """
htmlSanityCheck {
createBuildFile("""
failOnErrors = true
}
"""
""")

when:

Expand All @@ -90,14 +58,12 @@ class HtmlSanityCheckTaskFunctionalTest extends Specification {
given:
htmlFile << VALID_HTML
testProjectDir.newFile("test-invalid.html") << INVALID_HTML
buildFile << """
htmlSanityCheck {
createBuildFile("""
sourceDocuments = fileTree(sourceDir) {
include '**/$htmlFile.name'
}
failOnErrors = true
}
"""
""")

when:
def result = runnerForHtmlSanityCheckTask(gradleVersion).build()
Expand All @@ -109,17 +75,32 @@ class HtmlSanityCheckTaskFunctionalTest extends Specification {
gradleVersion << GRADLE_VERSIONS
}

@Unroll
def "should fail on invalid HTML/failOnErrors with Gradle version #gradleVersion"() {
given:
htmlFile << INVALID_HTML
createBuildFile("""
failOnErrors = true
""")

when:
def result = runnerForHtmlSanityCheckTask(gradleVersion).buildAndFail()

then:
result.task(":htmlSanityCheck").outcome == FAILED
result.output.contains("Your build configuration included 'failOnErrors=true', and 1 error(s) were found on all checked pages.")

where:
gradleVersion << GRADLE_VERSIONS
}

@Unroll
def "can select a subset of all checks to be performed with Gradle version #gradleVersion"() {
given:
htmlFile << VALID_HTML
buildFile << """
import org.aim42.htmlsanitycheck.check.AllCheckers
htmlSanityCheck {
checkerClasses = [AllCheckers.CHECKER_CLASSES.first()]
}
"""
createBuildFile("""
checkerClasses = [org.aim42.htmlsanitycheck.check.AllCheckers.CHECKER_CLASSES.first()]
""")

when:
runnerForHtmlSanityCheckTask(gradleVersion).build()
Expand All @@ -134,10 +115,10 @@ class HtmlSanityCheckTaskFunctionalTest extends Specification {

private GradleRunner runnerForHtmlSanityCheckTask(String gradleVersion) {
GradleRunner.create()
.withGradleVersion(gradleVersion)
.withProjectDir(testProjectDir.root)
.withPluginClasspath()
.withArguments('htmlSanityCheck')
.withGradleVersion(gradleVersion)
.withProjectDir(testProjectDir.root)
.withPluginClasspath()
.withArguments('htmlSanityCheck')
}

private static class HtmlReport {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.aim42.htmlsanitycheck

import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.testfixtures.ProjectBuilder
import spock.lang.Specification

class HtmlSanityCheckTaskSpec extends HtmlSanityCheckBaseSpec {
Project project
Task task

def setup () {
project = ProjectBuilder.builder().withProjectDir(testProjectDir.root).build()
task = project.tasks.register(HtmlSanityCheckPlugin.HTML_SANITY_CHECK, HtmlSanityCheckTask).get()
}

def "should initialize task with defaults"() {
expect:
task.failOnErrors == false
task.httpConnectionTimeout == 5000
task.ignoreLocalHost == false
task.ignoreIPAddresses == false
task.checkingResultsDir == new File(project.DEFAULT_BUILD_DIR_NAME, '/reports/htmlSanityCheck/')
task.junitResultsDir == new File(project.DEFAULT_BUILD_DIR_NAME, '/test-results/htmlSanityCheck/')
}

def "should work with simple file"() {
given:
htmlFile << VALID_HTML

when:
task.setSourceDir(testProjectDir.root)
task.httpSuccessCodes = [299]
task.httpErrorCodes = [599]
task.httpWarningCodes = [199]
task.sanityCheckHtml()

then:
task.sourceDocuments != null
}

def "should throw exception if configuration is invalid"() {
when:
task.sanityCheckHtml()

then:
def e = thrown(MisconfigurationException)
e.message.contains("source directory must not be null")
}
}

0 comments on commit 2ef70b3

Please sign in to comment.