2
0
Fork 0
mirror of https://github.com/ethauvin/rife2-hello.git synced 2025-04-25 07:27:12 -07:00

Fix reloading of templates

This commit fixes how templates were reloaded. There was a bug in
the plugin which used the output of the precompiled templates for
development only dependencies, instead of the templates directory.
This caused the templates to be always compiled and added as a
resource on runtime classpath, when we only wanted the raw templates.

This commit also adds functional tests to the build logic, which
can be executed by running `./gradlew build-logic:test`.
This commit is contained in:
Cedric Champeau 2023-03-05 13:38:42 +01:00
parent 65e579966c
commit c9f286132c
No known key found for this signature in database
GPG key ID: 825C06C827AF6B66
18 changed files with 542 additions and 11 deletions

View file

@ -33,16 +33,14 @@ repositories {
rife2 {
version.set("1.4.0")
useAgent.set(true)
precompiledTemplateTypes.addAll(HTML)
}
dependencies {
runtimeOnly("org.eclipse.jetty:jetty-server:11.0.13")
runtimeOnly("org.eclipse.jetty:jetty-servlet:11.0.13")
runtimeOnly("org.slf4j:slf4j-simple:2.0.5")
runtimeOnly(libs.bundles.jetty)
runtimeOnly(libs.slf4j.simple)
testImplementation("org.jsoup:jsoup:1.15.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
testImplementation(libs.jsoup)
testImplementation(libs.junit.jupiter)
}
tasks {

View file

@ -1,5 +1,6 @@
plugins {
`java-gradle-plugin`
groovy
}
repositories {
@ -8,6 +9,8 @@ repositories {
dependencies {
gradleApi()
testImplementation(libs.spock.core)
testImplementation(gradleTestKit())
}
gradlePlugin {
@ -18,3 +21,11 @@ gradlePlugin {
}
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf(org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED, org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED)
}
}

View file

@ -1 +1,9 @@
rootProject.name = "build-logic"
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View file

@ -45,6 +45,7 @@ public class Rife2Plugin implements Plugin<Project> {
public static final String DEFAULT_GENERATED_RIFE2_CLASSES_DIR = "generated/classes/rife2";
public static final String RIFE2_GROUP = "rife2";
public static final String WEBAPP_SRCDIR = "src/main/webapp";
public static final String PRECOMPILE_TEMPLATES_TASK_NAME = "precompileTemplates";
@Override
public void apply(Project project) {
@ -62,7 +63,7 @@ public class Rife2Plugin implements Plugin<Project> {
configurations.getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME).extendsFrom(rife2Configuration);
var precompileTemplates = registerPrecompileTemplateTask(project, rife2CompilerClasspath, rife2Extension);
createRife2DevelopmentOnlyConfiguration(project, configurations, dependencyHandler, precompileTemplates);
createRife2DevelopmentOnlyConfiguration(project, configurations, dependencyHandler);
exposePrecompiledTemplatesToTestTask(project, configurations, dependencyHandler, precompileTemplates);
configureAgent(project, plugins, rife2Extension, rife2AgentClasspath);
TaskProvider<Jar> uberJarTask = registerUberJarTask(project, plugins, javaPluginExtension, rife2Extension, tasks, precompileTemplates);
@ -118,14 +119,13 @@ public class Rife2Plugin implements Plugin<Project> {
private void createRife2DevelopmentOnlyConfiguration(Project project,
ConfigurationContainer configurations,
DependencyHandler dependencies,
TaskProvider<PrecompileTemplates> precompileTemplatesTask) {
DependencyHandler dependencies) {
var rife2DevelopmentOnly = configurations.create("rife2DevelopmentOnly", conf -> {
conf.setDescription("Dependencies which should only be visible when running the application in development mode (and not in tests).");
conf.setCanBeConsumed(false);
conf.setCanBeResolved(false);
});
rife2DevelopmentOnly.getDependencies().add(dependencies.create(project.files(precompileTemplatesTask)));
rife2DevelopmentOnly.getDependencies().add(dependencies.create(project.files(DEFAULT_TEMPLATES_DIR)));
configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME).extendsFrom(rife2DevelopmentOnly);
}
@ -174,6 +174,7 @@ public class Rife2Plugin implements Plugin<Project> {
rife2.getUseAgent().convention(false);
rife2.getUberMainClass().convention(project.getExtensions().getByType(JavaApplication.class).getMainClass()
.map(mainClass -> mainClass + "Uber"));
rife2.getPrecompiledTemplateTypes().convention(Collections.singletonList(TemplateType.HTML));
return rife2;
}
@ -216,7 +217,7 @@ public class Rife2Plugin implements Plugin<Project> {
private static TaskProvider<PrecompileTemplates> registerPrecompileTemplateTask(Project project,
Configuration rife2CompilerClasspath,
Rife2Extension rife2Extension) {
return project.getTasks().register("precompileTemplates", PrecompileTemplates.class, task -> {
return project.getTasks().register(PRECOMPILE_TEMPLATES_TASK_NAME, PrecompileTemplates.class, task -> {
task.setGroup(RIFE2_GROUP);
task.setDescription("Pre-compiles the templates.");
task.getVerbose().convention(true);

View file

@ -0,0 +1,40 @@
import com.uwyn.rife2.gradle.TemplateType.*
plugins {
id("application")
id("com.uwyn.rife2")
}
base {
archivesName = "hello"
version = 1.0
group = "com.example"
}
application {
mainClass = "hello.App"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
rife2 {
version = "1.4.0"
useAgent = true
}
dependencies {
runtimeOnly("org.eclipse.jetty:jetty-server:11.0.13")
runtimeOnly("org.eclipse.jetty:jetty-servlet:11.0.13")
runtimeOnly("org.slf4j:slf4j-simple:2.0.5")
testImplementation("org.jsoup:jsoup:1.15.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
}

View file

@ -0,0 +1 @@
rootProject.name = 'minimal'

View file

@ -0,0 +1,16 @@
package hello;
import rife.engine.*;
public class App extends Site {
public void setup() {
var hello = get("/hello", c -> c.print(c.template("hello")));
get("/", c -> c.redirect(hello));
}
public static void main(String[] args) {
new Server()
.staticResourceBase("src/main/webapp")
.start(new App());
}
}

View file

@ -0,0 +1,11 @@
package hello;
import rife.engine.Server;
public class AppUber extends App {
public static void main(String[] args) {
new Server()
.staticUberJarResourceBase("webapp")
.start(new AppUber());
}
}

View file

@ -0,0 +1,6 @@
[
{
"name":"rife.template.html.hello",
"methods":[{"name":"<init>","parameterTypes":[] }]
}
]

View file

@ -0,0 +1,8 @@
{
"resources":{
"includes":[
{"pattern":"^webapp/.*$"}
]
},
"bundles":[]
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><!--v title-->Hello<!--/v--></title>
<link rel="stylesheet" href="{{v webapp:rootUrl/}}css/style.css?{{v context:paramRandom/}}">
</head>
<body>
<p>Hello World</p>
</body>
</html>

View file

@ -0,0 +1,21 @@
:root {
/* fonts */
--main-font: sans-serif;
/* font sizes */
--main-font-size: 18px;
/* colors */
--main-background-color: #0d0d0d;
--main-text-color: #d0d0d0;
/* margins and padding */
--content-padding: 2em;
}
body {
background: var(--main-background-color);
font-family: var(--main-font);
font-style: var(--main-font-size);
color: var(--main-text-color);
padding: var(--content-padding);
}

View file

@ -0,0 +1,24 @@
/*
* This Java source file was generated by the Gradle 'init' task.
*/
package hello;
import org.junit.jupiter.api.Test;
import rife.test.MockConversation;
import static org.junit.jupiter.api.Assertions.*;
public class AppTest {
@Test
void verifyRoot() {
var m = new MockConversation(new App());
assertEquals(m.doRequest("/").getStatus(), 302);
}
@Test
void verifyHello() {
var m = new MockConversation(new App());
assertEquals("Hello", m.doRequest("/hello")
.getTemplate().getValue("title"));
}
}

View file

@ -0,0 +1,184 @@
package com.uwyn.rife2.gradle
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.gradle.util.GFileUtils
import org.gradle.util.GradleVersion
import spock.lang.Specification
import spock.lang.TempDir
import java.nio.file.Path
abstract class AbstractFunctionalTest extends Specification {
private final String gradleVersion = System.getProperty("gradleVersion", GradleVersion.current().version)
@TempDir
Path testDirectory
boolean debug
private StringWriter outputWriter
private StringWriter errorOutputWriter
private String output
private String errorOutput
BuildResult result
Path path(String... pathElements) {
Path cur = testDirectory
pathElements.each {
cur = cur.resolve(it)
}
cur
}
File file(String... pathElements) {
path(pathElements).toFile()
}
File getGroovyBuildFile() {
file("build.gradle")
}
File getBuildFile() {
groovyBuildFile
}
File getKotlinBuildFile() {
file("build.gradle.kts")
}
File getGroovySettingsFile() {
file("settings.gradle")
}
File getKotlinSettingsFile() {
file("settings.gradle.kts")
}
File getSettingsFile() {
groovySettingsFile
}
void run(String... args) {
try {
result = newRunner(args)
.build()
} finally {
recordOutputs()
}
}
void outputContains(String text) {
assert output.normalize().contains(text.normalize())
}
void outputDoesNotContain(String text) {
assert !output.normalize().contains(text.normalize())
}
void errorOutputContains(String text) {
assert errorOutput.normalize().contains(text.normalize())
}
void tasks(@DelegatesTo(value = TaskExecutionGraph, strategy = Closure.DELEGATE_FIRST) Closure spec) {
def graph = new TaskExecutionGraph()
spec.delegate = graph
spec.resolveStrategy = Closure.DELEGATE_FIRST
spec()
}
private void recordOutputs() {
output = outputWriter.toString()
errorOutput = errorOutputWriter.toString()
}
private GradleRunner newRunner(String... args) {
outputWriter = new StringWriter()
errorOutputWriter = new StringWriter()
ArrayList<String> autoArgs = computeAutoArgs()
def runner = GradleRunner.create()
.forwardStdOutput(tee(new OutputStreamWriter(System.out), outputWriter))
.forwardStdError(tee(new OutputStreamWriter(System.err), errorOutputWriter))
.withPluginClasspath()
.withProjectDir(testDirectory.toFile())
.withArguments([*autoArgs, *args])
if (gradleVersion) {
runner.withGradleVersion(gradleVersion)
}
if (debug) {
runner.withDebug(true)
}
runner
}
private ArrayList<String> computeAutoArgs() {
List<String> autoArgs = [
"-s",
"--console=verbose"
]
if (Boolean.getBoolean("config.cache")) {
autoArgs << '--configuration-cache'
}
autoArgs
}
private static Writer tee(Writer one, Writer two) {
return TeeWriter.of(one, two)
}
void fails(String... args) {
try {
result = newRunner(args)
.buildAndFail()
} finally {
recordOutputs()
}
}
private class TaskExecutionGraph {
void succeeded(String... tasks) {
tasks.each { task ->
contains(task)
assert result.task(task).outcome == TaskOutcome.SUCCESS
}
}
void failed(String... tasks) {
tasks.each { task ->
contains(task)
assert result.task(task).outcome == TaskOutcome.FAILED
}
}
void skipped(String... tasks) {
tasks.each { task ->
contains(task)
assert result.task(task).outcome == TaskOutcome.SKIPPED
}
}
void contains(String... tasks) {
tasks.each { task ->
assert result.task(task) != null: "Expected to find task $task in the graph but it was missing"
}
}
void doesNotContain(String... tasks) {
tasks.each { task ->
assert result.task(task) == null: "Task $task should be missing from the task graph but it was found with an outcome of ${result.task(task).outcome}"
}
}
}
void usesProject(String name) {
File sampleDir = new File("src/test-projects/$name")
GFileUtils.copyDirectory(sampleDir, testDirectory.toFile())
}
File file(String path) {
new File(testDirectory.toFile(), path)
}
}

View file

@ -0,0 +1,35 @@
package com.uwyn.rife2.gradle
import java.nio.file.FileSystems
import java.nio.file.Files
class PackagingTest extends AbstractFunctionalTest {
def setup() {
usesProject("minimal")
}
def "#archive contains compiled resources"() {
def jarFile = file(archive).toPath()
when:
run task
then: "compiles templates are found in the archive"
tasks {
succeeded ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}"
}
Files.exists(jarFile)
try (def fs = FileSystems.newFileSystem(jarFile, [:])) {
fs.getRootDirectories().each {
Files.walk(it).forEach { path ->
println path
}
}
assert Files.exists(fs.getPath("/rife/template/html/hello.class"))
}
where:
task | archive
'jar' | 'build/libs/hello-1.0.jar'
'uberJar' | 'build/libs/hello-uber-1.0.jar'
}
}

View file

@ -0,0 +1,81 @@
package com.uwyn.rife2.gradle
import groovy.transform.CompileStatic
@CompileStatic
class TeeWriter extends Writer {
private final Writer one
private final Writer two
static TeeWriter of(Writer one, Writer two) {
new TeeWriter(one, two)
}
private TeeWriter(Writer one, Writer two) {
this.one = one
this.two = two
}
@Override
void write(int c) throws IOException {
try {
one.write(c)
} finally {
two.write(c)
}
}
@Override
void write(char[] cbuf) throws IOException {
try {
one.write(cbuf)
} finally {
two.write(cbuf)
}
}
@Override
void write(char[] cbuf, int off, int len) throws IOException {
try {
one.write(cbuf, off, len)
} finally {
two.write(cbuf, off, len)
}
}
@Override
void write(String str) throws IOException {
try {
one.write(str)
} finally {
two.write(str)
}
}
@Override
void write(String str, int off, int len) throws IOException {
try {
one.write(str, off, len)
} finally {
two.write(str, off, len)
}
}
@Override
void flush() throws IOException {
try {
one.flush()
} finally {
two.flush()
}
}
@Override
void close() throws IOException {
try {
one.close()
} finally {
two.close()
}
}
}

View file

@ -0,0 +1,58 @@
package com.uwyn.rife2.gradle
class TemplateCompilationTest extends AbstractFunctionalTest {
def setup() {
usesProject("minimal")
}
def "doesn't precompile templates when calling `run`"() {
given:
buildFile << """
tasks.named("run") {
doFirst {
throw new RuntimeException("force stop")
}
}
"""
when:
fails 'run'
then: "precompile templates task must not be present in task graph"
errorOutputContains("force stop")
tasks {
doesNotContain ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}"
}
}
def "`run` task classpath includes template sources"() {
given:
buildFile << """
tasks.register("dumpRunClasspath") {
doLast {
tasks.named("run").get().classpath.files.each {
println "Classpath entry: \$it"
}
}
}
"""
when:
run("dumpRunClasspath")
then: "template sources must be present in the classpath"
outputContains("Classpath entry: ${file("src/main/templates").absolutePath}")
}
def "compiles templates when running #task"() {
when:
run task
then: "precompile templates task must be present in task graph"
tasks {
succeeded ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}"
}
where:
task << ['jar', 'test', 'uberJar']
}
}

17
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,17 @@
[versions]
jetty = "11.0.13"
jsoup = "1.15.3"
junit-jupiter = "5.9.1"
slf4j = "2.0.5"
spock = "2.3-groovy-3.0"
[libraries]
jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" }
jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" }
[bundles]
jetty = [ "jetty-server", "jetty-servlet" ]