Skip to content

Various Cleanup prior to Sitemesh 2 revert #14876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f51194e
test: #14193 add test case for changes after forward
jdaugherty Jun 30, 2025
e49e178
test: add back sitemesh tests that were removed & rename tests to gsp…
jdaugherty Jun 30, 2025
75fe506
test: add wait to fix randomly failing test
jdaugherty Jul 1, 2025
e48c026
test: add script to assist in finding test pollution
jdaugherty Jul 1, 2025
cab0304
fix: do not recreate the grails application while trying to clear it
jdaugherty Jul 1, 2025
1f9db1c
test: fix test pollution with taglibs
jdaugherty Jul 1, 2025
ea62cf8
feedback: cleanup
jdaugherty Jul 4, 2025
d87b0e0
cleanup: remove sitemesh from package/test names
jdaugherty Jul 4, 2025
cbe8018
cleanup: mockEmbeddedSitemeshLayout -> mockEmbeddedGrailsLayout
jdaugherty Jul 4, 2025
de701f4
fix: tests require databind; do not transitively include
jdaugherty Jul 4, 2025
4f07ccb
test: convert to specification
jdaugherty Jul 4, 2025
ade8dbc
test: add missing sitemesh2 tests
jdaugherty Jul 4, 2025
59a618b
test: add test for no layout at the controller level
jdaugherty Jul 4, 2025
76a122f
docs: update docs to not include Sitemesh in the name
jdaugherty Jul 4, 2025
b22d747
cleanup: use StandardCharsets
jdaugherty Jul 4, 2025
25638ca
refactor: move RENDERING_VIEW_ATTRIBUTE to GroovyPagesUriService
jdaugherty Jul 4, 2025
7783899
chore: reformat code
jdaugherty Jul 4, 2025
e8f7c76
refactor: introduce common interface to customize view in rendering
jdaugherty Jul 4, 2025
8e0b9ca
refactor: introduce common interface to customize layout in rendering
jdaugherty Jul 4, 2025
24db59d
refactor: decouple further response processing in exception resolving
jdaugherty Jul 4, 2025
514bb2a
feedback: styling
jdaugherty Jul 5, 2025
7d2e916
revert: some styling changes that cause test failures
jdaugherty Jul 5, 2025
c69a8a4
feedback: further code styling cleanup
jdaugherty Jul 5, 2025
06031d5
refactor: move GrailsResponseMutator to grails-web-common
jdaugherty Jul 5, 2025
69f1cf4
feedback: styling
jdaugherty Jul 6, 2025
146872d
revert double quote conversion
jdaugherty Jul 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions etc/bin/test-bisect.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#!/usr/bin/env groovy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import java.nio.file.Path
import java.nio.file.Paths
import groovy.cli.commons.CliBuilder

/**
* This script finds the test class that's causing test pollution in other tests.
* To use it, gradle must be configured to run sequentially, without retries, and output a specific line
* so the script knows the test order. The script will pass the property `testBisect` for gradle to detect when being called
* by this script. For example:
*
* if(hasProperty('testBisect')) {
* tasks.withType(Test).configureEach {
* maxParallelForks = 1
* forkEvery = 0
* develocity {
* testDistribution {
* enabled.set(false)
* retryInSameJvm.set(true)
* }
* testRetry {
* maxRetries = 1
* maxFailures = 1
* failOnPassedAfterRetry = true
* }
* }
* beforeTest { testDescriptor ->
* logger.info "STARTED TEST: ${testDescriptor.className} > ${testDescriptor.name}"
* }
* afterSuite { desc, result ->
* if (!desc.parent) {
* logger.info("FINISHED - ${result.failedTestCount == 0 && result.testCount > 1} ? 'PASSED' : 'FAILED'} - ${result.testCount} tests executed, ${result.failedTestCount} failed")
* }
* }
* }
* }
*/
class TestPollution {

File gradleCommandDirectory
String taskPath
String testName

Path scriptDir = Paths.get(getClass()
.protectionDomain
.codeSource
.location
.toURI())
.toAbsolutePath()
.parent

TestPollution(args) {
parseArgs(args)
}

def parseArgs(args) {
def cli = new CliBuilder(usage: "test-bisect.groovy -p :grails-gsp:test -n org.grails.web.taglib.FormTagLibTests")

cli.with {
p longOpt: 'taskPath', args: 1, argName: 'taskPath', 'The path to the gradle test task from the root of the project directory, e.g. grails-gsp:test'
n longOpt: 'testName', args: 1, argName: 'testName', 'The test class name that passes when run by itself but fails when the other tests are run'
}

def options = cli.parse(args)

if (options.'taskPath') {
taskPath = options.'taskPath'
}

if (options.'testName') {
testName = options.'testName'
}

if (!taskPath || !testName) {
cli.usage()
System.exit(-1)
}

gradleCommandDirectory = scriptDir.resolve('../..').toFile().canonicalFile
if (!gradleCommandDirectory.exists()) {
println "Unable to find gradle rootProject for the given path: $gradleCommandDirectory.toString()"
cli.usage()
System.exit(-1)
}
}

Collection<String> findTestRunOrder() {
LinkedHashSet<String> tests = []

def pattern = ~/.*STARTED TEST: ([a-z0-9A-Z.]+) >.*/
runCommand('test-run-order', "./gradlew ${taskPath} --rerun-tasks -PtestBisect --info --no-daemon --no-parallel") { String line ->
pattern.matcher(line).each { matcher ->
if (matcher.size() > 1) {
String testName = matcher[1]
if (testName && !tests.contains(testName)) {
tests << testName
}
}
}
}

tests
}

void bisectTests() {
if (!testsPass("solo", [testName])) {
println "The test ($testName) fails when run by itself. Pollution cannot be determined."
println "Run the command yourself: ${createCommand(withPollutedTest([]))}"
return
}

List<String> testList = (findTestRunOrder() - testName).toList()
if (!testList) {
println "No other tests were found. Cannot bisect."
System.exit(1)
}

bisectTests(testList)
}

List<String> bisectTests(List<String> testList) {

// if the test list size is > 1, split into left and right hand sides
if (testList.size() > 1) {
List<String> lhs = leftHandSide(testList)
List<String> rhs = rightHandSide(testList)

if (!testsPass("left", withPollutedTest(lhs))) {
return bisectTests(lhs)
}

if (!testsPass("right", withPollutedTest(rhs))) {
return bisectTests(rhs)
}

if (testsPass("full", withPollutedTest(testList))) {
println "Unable to find any failing tests with this list, running them all appears to run without issue: \n${testList.join(" ")}"
return null
}

println "Unable to find just one test that's causing problems. Running just with the left hand or right hand side of this narrowed list passes, but the full list fails"
println "full list (fails): ${testList.join(" ")}"
println "left hand side (passes): ${lhs.join(" ")}"
println "right hand side (passes): ${rhs.join(" ")}"
} else if (!testsPass("suspected", withPollutedTest(testList))) {
println "The test that's causing pollution: ${testList.join(" ")}" // should only be 1
println "Here's the command to execute to see if you've fixed things:\n ${createCommand(withPollutedTest(testList))}"
return testList
}

println "Not sure what's happening, got to this list of tests, but everything passes with this list: \n${testList.join(" ")}"
[]
}

List<String> leftHandSide(List<String> testList) {
return testList[0..(testList.size() / 2 - 1)]
}

List<String> rightHandSide(List<String> testList) {
return testList[(testList.size() / 2)..-1]
}

List<String> withPollutedTest(List<String> testList) {
return [testList, testName].flatten()
}

Boolean testsPass(String runName, List<String> testList) {
String command = createCommand(testList)
return runCommand(runName, command)
}

String createCommand(List<String> testList) {
def testArgs = testList.collect { "--tests ${it}" }
"./gradlew ${taskPath} --rerun-tasks -PtestBisect --no-daemon --no-parallel --info ${testArgs.join(' ')}"
}

def out(prefix, message) {
println("${prefix.padLeft(16, ' ')}: $message")
}

boolean runCommand(runName, command, Closure outputParser = null) {
out runName, command
String completedLine = ''

String knownExecutor = null
def executorPattern = ~/.*Gradle Test Executor ([0-9]+) .*/
def finishPattern = ~/.*FINISHED - (PASSED|FAILED) - .*/
def exitValue = command.execute(null, gradleCommandDirectory).with { proc ->
proc.in.eachLine { String line ->
// uncomment for real time output of the gradle commands
// out(runName, line)
def completedMatcher = finishPattern.matcher(line)
if (completedMatcher.matches()) {
completedLine = line
}


def execMatcher = executorPattern.matcher(line)
if (execMatcher.matches()) {
if (execMatcher.size() > 1) {
def foundExecutor = execMatcher[1]
if (!knownExecutor && foundExecutor) {
knownExecutor = foundExecutor
} else if (knownExecutor != foundExecutor) {
throw new Exception("WARNING: Found multiple Gradle Test Executors: ${knownExecutor} and ${foundExecutor}. Test pollution detection cannot work if tests are run in parallel.")
}
}
}

if (outputParser) {
outputParser.call(line)
}
}
proc.waitFor()
proc.exitValue()
}

if(!completedLine) {
throw new IllegalStateException("Could not locate the completed line in the output.")
}
boolean failures = completedLine.contains('FAILED')

out(runName, "exitValue=${exitValue}, failures=${failures}, testExecutor=${knownExecutor}, completedLine=${completedLine}")
!failures && exitValue == 0
}
}

new TestPollution(args).bisectTests()
33 changes: 31 additions & 2 deletions gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ tasks.withType(Test).configureEach {
jvmArgs += java17moduleReflectionCompatibilityArguments
develocity {
testRetry {
maxRetries = 2
maxRetries = configuredTestParallel == 1 ? 1 : 2
maxFailures = 20
failOnPassedAfterRetry = true
}
Expand All @@ -63,7 +63,7 @@ tasks.withType(Test).configureEach {
excludes = ['**/*TestCase.class', '**/*$*.class']
maxParallelForks = configuredTestParallel
maxHeapSize = isCiBuild ? '768m' : '1024m'
forkEvery = isCiBuild ? 20 : 100
forkEvery = hasProperty('forkEveryUnitTest') ? getProperty('forkEveryUnitTest') as long : (isCiBuild ? 20 : 100)
if (System.getProperty('debug.tests')) {
jvmArgs += debugArguments
}
Expand All @@ -73,3 +73,32 @@ tasks.withType(Test).configureEach {
}
}

if (hasProperty('testBisect')) {
tasks.withType(Test).configureEach {
maxParallelForks = 1
forkEvery = 0

develocity {
testDistribution {
enabled = false
retryInSameJvm = true
}
testRetry {
maxRetries = 1
maxFailures = 1
failOnPassedAfterRetry = true
}
}

beforeTest { testDescriptor ->
logger.info('STARTED TEST: {} > {}', testDescriptor.className, testDescriptor.name)
}

afterSuite { desc, result ->
if (!desc.parent) {
def testStatus = result.failedTestCount == 0 && result.testCount > 1 ? 'PASSED' : 'FAILED'
logger.info('FINISHED - {} - {} tests executed, {} failed', testStatus, result.testCount, result.failedTestCount)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ package org.grails.plugins.codecs
import org.apache.commons.codec.binary.Base64
import org.codehaus.groovy.runtime.NullObject

import java.nio.charset.StandardCharsets

/**
* A codec that encodes and decodes Objects using Base64 encoding.
*
Expand All @@ -37,7 +39,7 @@ class Base64CodecExtensionMethods {
return new String(Base64.encodeBase64(theTarget))
}

return new String(Base64.encodeBase64(theTarget.toString().getBytes("UTF-8")))
return new String(Base64.encodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8)))
}

static decodeBase64(theTarget) {
Expand All @@ -49,6 +51,6 @@ class Base64CodecExtensionMethods {
return Base64.decodeBase64(theTarget)
}

return Base64.decodeBase64(theTarget.toString().getBytes("UTF-8"))
return Base64.decodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.grails.plugins.codecs

import java.nio.charset.StandardCharsets
import java.security.MessageDigest

abstract class DigestUtils {
Expand All @@ -38,7 +39,7 @@ abstract class DigestUtils {
data.eachWithIndex { v, i -> src[i] = v }
}
else {
src = data.toString().getBytes("UTF-8")
src = data.toString().getBytes(StandardCharsets.UTF_8)
}
md.update(src) // This probably needs to use the thread's Locale encoding
return md.digest()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ package org.grails.plugins.codecs

import org.codehaus.groovy.runtime.NullObject

import java.nio.charset.StandardCharsets

class HexCodecExtensionMethods {

static HEXDIGITS = '0123456789abcdef'
Expand All @@ -32,7 +34,7 @@ class HexCodecExtensionMethods {

def result = new StringBuilder()
if (theTarget instanceof String) {
theTarget = theTarget.getBytes("UTF-8")
theTarget = theTarget.getBytes(StandardCharsets.UTF_8)
}
theTarget.each() {
result << HexCodecExtensionMethods.HEXDIGITS[(it & 0xF0) >> 4]
Expand Down
Loading
Loading