Skip to content

Feature/groovy property expansion#657

Open
rschwietzke wants to merge 4 commits intodevelopfrom
feature/groovy-property-expansion
Open

Feature/groovy property expansion#657
rschwietzke wants to merge 4 commits intodevelopfrom
feature/groovy-property-expansion

Conversation

@rschwietzke
Copy link
Contributor

Property Expansion with Groovy Expressions

XLT properties support dynamic value expansion using Groovy expressions. This allows you to calculate load mixes, define relationships between properties, and share computed values across your configuration.

Getting Started

In any properties file, you can use two expansion syntaxes:

  • ${property.name} - Standard variable substitution (references other properties)
  • #{...} - Groovy expression evaluation (Spring-like syntax)
# Standard variable substitution
base.url = https://example.com
login.url = ${base.url}/login

# Groovy expression
totalUsers = 100
com.xceptance.xlt.loadtests.TBrowse.users = #{ (props['totalUsers'] as int) * 0.4 }

Evaluation Order

  1. ${...} variable references are resolved first
  2. #{...} Groovy expressions are evaluated second

Since ${} is resolved before Groovy evaluation, you can use property values directly without needing props:

maxUsers = 100

# Preferred: use ${} for simple property access
browse.users = #{ ${maxUsers} * 0.4 }  # Results in: 40.0

# Equivalent but more verbose
browse.users = #{ props['maxUsers'] as int * 0.4 }

Tip: Use ${} when you just need the property value. Use props only when you need dynamic key lookup or default values.

Available Bindings

Groovy expressions have access to two variables:

props - Property Access (When Needed)

Use props when you need dynamic key lookup or default values:

# Dynamic key lookup
env = production
value = #{ props.getProperty("limit.${props['env']}", '100') as int }

# Default values
orderUsers = #{ props.getProperty('order.users', '10') as int }

For simple property access, prefer ${} syntax instead:

totalUsers = 100

# Preferred
browseUsers = #{ ${totalUsers} * 0.4 }

# Also works, but more verbose
browseUsers = #{ props['totalUsers'] as int * 0.4 }

Multi-Line Scripts

Groovy expressions can span multiple lines:

com.xceptance.xlt.loadtests.config = #{ \
    def total = props['totalUsers'] as int \
    def browse = (total * 0.4) as int \
    def order = (total * 0.1) as int \
    \
    ctx['totalUsers'] = total \
    ctx['browseUsers'] = browse \
    ctx['orderUsers'] = order \
    \
    'configured' \
}

com.xceptance.xlt.loadtests.TBrowse.users = #{ ctx['browseUsers'] }
com.xceptance.xlt.loadtests.TOrder.users = #{ ctx['orderUsers'] }

Note: Java properties do not support multi-line text directly. If you need to use multi-line scripts, you must use the \ line ending to tell the properties file parser that the data continues on the next line.

ctx - Shared Context Map

Store and share values between expressions:

# Store computed values
init = #{ \
    ctx['base'] = props['totalUsers'] as int \
    ctx['loaded'] = true \
    'initialized' \
}

# Use stored values later
browse.users = #{ ctx['base'] * 0.4 }
order.users = #{ ctx['base'] * 0.1 }

Practical Examples

Load Mix Calculation

# Base configuration
totalUsers = 100

# Calculate user distribution using ${} (preferred)
com.xceptance.xlt.loadtests.TBrowse.users = #{ ${totalUsers} * 0.40 as int }
com.xceptance.xlt.loadtests.TSearch.users = #{ ${totalUsers} * 0.30 as int }
com.xceptance.xlt.loadtests.TOrder.users = #{ ${totalUsers} * 0.10 as int }
com.xceptance.xlt.loadtests.TCheckout.users = #{ ${totalUsers} * 0.20 as int }

Environment-Based Configuration

# Environment multiplier (override per environment)
load.multiplier = 1.0
base.browse.users = 50

# Scale by multiplier using ${}
com.xceptance.xlt.loadtests.TBrowse.users = #{ ${base.browse.users} * ${load.multiplier} as int }

Dynamic Arrival Rates

hourlyRequests = 10000
testDurationHours = 1

# Calculate arrival rate per hour
com.xceptance.xlt.loadtests.TSearch.arrivalRate = #{ \
    props['hourlyRequests'] as int \
}

Supported Operations

Groovy expressions support:

  • Arithmetic: +, -, *, /, %
  • Type conversion: as int, as double, as String
  • String operations: concatenation, interpolation
  • Collections: Lists, Maps
  • Closures: Functional operations
  • Conditionals: if/else, ternary operator
# Conditionals
mode = production
users = #{ props['mode'] == 'production' ? 100 : 10 }

# String operations
env.label = #{ "Environment: ${props['mode']}" }

Security

Groovy scripts run in a sandbox. The following operations are blocked:

  • File system access (File, FileReader, etc.)
  • Network operations (URL, Socket, etc.)
  • System access (System, Runtime)
  • Thread creation
  • Reflection

Only safe imports are allowed: java.util.*, java.math.*, java.text.*

Error Handling

Invalid Groovy expressions throw IllegalArgumentException with the script content and error message:

Failed to evaluate Groovy expression: #{invalid syntax} - ...

Tips

  1. Use as int for integer results (Groovy defaults to BigDecimal for division)
  2. Store complex calculations in ctx to avoid repetition
  3. Initialize early - put setup expressions in properties that load first
  4. Test expressions - verify calculations produce expected values

@socket-security
Copy link

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedmaven/​org.apache.groovy/​groovy@​5.0.410010090100100

View full report

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for Groovy expression evaluation in XLT property values using #{...} syntax, enabling dynamic property calculations and load mix configurations. Variable substitution with ${...} is resolved first, followed by Groovy expression evaluation with access to props and ctx bindings.

Changes:

  • Added Groovy expression evaluation support with sandboxed security restrictions
  • Introduced shared context map for storing computed values between expressions
  • Extended existing variable substitution to support two-phase expansion

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/main/java/com/xceptance/common/util/GroovyPropertyEvaluator.java Implements Groovy expression parsing and evaluation with security restrictions
src/main/java/com/xceptance/common/util/PropertyGroovySecurityUtils.java Configures security sandbox for Groovy script execution
src/main/java/com/xceptance/common/util/PropertiesUtils.java Modified to support two-phase property expansion (variables then Groovy)
src/test/java/com/xceptance/common/util/PropertiesUtilsTest.java Added comprehensive test coverage for Groovy expression evaluation
pom.xml Added Apache Groovy 5.0.4 dependency
doc/3rd-party-licenses/groovy/* Added Apache Groovy license and notice files
PROPERTY_EXPANSION.md Complete documentation for property expansion feature
FOR_RELEASE.md Deleted unrelated release note

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +163 to +168
final Script scriptInstance;
synchronized (compiledScript)
{
// Clone the script for thread safety
scriptInstance = compiledScript.getClass().getDeclaredConstructor().newInstance();
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The synchronized block on compiledScript does not provide thread safety for script instantiation. The getDeclaredConstructor().newInstance() call creates a new instance without modifying the original script, making the synchronization unnecessary. Remove the synchronized block around the instantiation.

Suggested change
final Script scriptInstance;
synchronized (compiledScript)
{
// Clone the script for thread safety
scriptInstance = compiledScript.getClass().getDeclaredConstructor().newInstance();
}
// Create a new script instance for thread safety; no synchronization needed
final Script scriptInstance = compiledScript.getClass().getDeclaredConstructor().newInstance();

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +158
synchronized (SHELL)
{
compiledScript = SHELL.parse(script);
SCRIPT_CACHE.put(script, compiledScript);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double-checked locking pattern is incomplete. After the first null check at line 153, another thread could populate the cache before entering the synchronized block. Add a second null check inside the synchronized block to prevent duplicate script compilation.

Suggested change
synchronized (SHELL)
{
compiledScript = SHELL.parse(script);
SCRIPT_CACHE.put(script, compiledScript);
// Double-checked locking to avoid duplicate compilation of the same script.
synchronized (SHELL)
{
// Re-check after acquiring the lock in case another thread already compiled it.
compiledScript = SCRIPT_CACHE.get(script);
if (compiledScript == null)
{
compiledScript = SHELL.parse(script);
SCRIPT_CACHE.put(script, compiledScript);
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support dynamic properties especially for load test configuration

2 participants