I have sample code as below
import org.codehaus.groovy.control.CompilerConfiguration
abstract class MyClass extends Script {
void testMethod(Integer x) {
println "x = $x"
}
}
public static void main(String[] args) {
compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.setScriptBaseClass("MyClass");
GroovyShell shell = new GroovyShell(new Binding(), compilerConfiguration);
shell.evaluate("testMethod 1")
}
When I run this class it prints x = 1
now if I change the "testMethod 1" to "testMethod -1" it fails with
Caught: groovy.lang.MissingPropertyException: No such property: testMethod for class: Script1
groovy.lang.MissingPropertyException: No such property: testMethod for class: Script1
at Script1.run(Script1.groovy:1)
at Test.run(Test.groovy:15)
Now I change "testMethod -1" to "testMethod (-1)". It again works and printed x = -1
What I need to understand is why Groovy is asking for the parentheses for negative numbers.
Because without parentheses, it is assuming you are trying to subtract 1 from a property called testMethod (ie: testMethod - 1)
You need the parentheses to inform the parser that this is a method call rather than a subtraction operation
Edit
I came up with a horrible way to get this to work:
import java.lang.reflect.Method
import org.codehaus.groovy.control.CompilerConfiguration
abstract class MyClass extends Script {
private methods = [:]
class MinusableMethod {
Script declarer
Method method
MinusableMethod( Script d, Method m ) {
this.declarer = d
this.method = m
}
def minus( amount ) {
method.invoke( declarer, -amount )
}
}
public MyClass() {
super()
methods = MyClass.getDeclaredMethods().grep {
it.name != 'propertyMissing' && !it.synthetic
}.collectEntries {
[ (it.name): new MinusableMethod( this, it ) ]
}
}
def propertyMissing( String name ) {
methods[ name ]
}
void testMethod(Integer x) {
println "x = $x"
}
}
static main( args ) {
def compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.setScriptBaseClass( 'MyClass' );
GroovyShell shell = new GroovyShell(new Binding(), compilerConfiguration);
shell.evaluate("testMethod - 1")
}
But this will probably break under other conditions
In the long run, getting people to write valid scripts is probably the better route to take...
Related
I need to create a Script from a String and execute it in the context of the current test class. Here's my simplified code:
import spock.lang.Specification
class MyTestSpec extends Specification {
Integer getOne() { return 1 }
Integer getTwo() { return 2 }
void 'call script with local methods'() {
given:
GroovyShell shell = new GroovyShell()
Script script = shell.parse("getOne() + getTwo()")
when:
def result = script.run()
then:
result == 3
}
}
This gives me the following error:
No signature of method: Script1.getOne() is applicable for argument types: () values: []
I see that to set variables one can use shell.setProperty but how do I pass the method's implementation to the script?
Of course, as soon as I posted this, I found my answer.
import org.codehaus.groovy.control.CompilerConfiguration
import spock.lang.Specification
class MyTestSpec extends Specification {
Integer getOne() { return 1 }
Integer getTwo() { return 2 }
void 'call script with local methods'() {
given:
CompilerConfiguration cc = new CompilerConfiguration()
cc.setScriptBaseClass(DelegatingScript.name)
GroovyShell sh = new GroovyShell(this.class.classLoader, new Binding(), cc)
DelegatingScript script = (DelegatingScript) sh.parse("getOne() + getTwo()")
script.setDelegate(this)
when:
def result = script.run()
then:
result == 3
}
}
How can I define a class in a Jenkins shared library that contains an empty JSON object?
// src/org/build/Report.groovy
package org.build
public class Report implements Serializable {
def steps
def json
Report(steps) {
this.steps = steps
this.json = emptyJson()
}
#NonCPS
def emptyJson() {
return this.steps.readJSON( text: '{}' )
}
}
...is instantiated from this pipeline:
#Library('my-library')
import org.build.Report
pipeline {
agent any
stages {
stage("foo") {
steps {
script {
rep = new org.build.Report(this)
}
}
}
}
}
...and fails with the error: expected to call org.build.Report.<init> but wound up catching readJSON; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/
I had only earlier today thought I'd figured out how to solve this "class" of problem.
Earlier, I encountered the same error when invoking a shared-library function from a shared-library class. I fixed that problem per the guidance at the link that the error message noted, i.e. annotating the shared-library function with #NonCPS.
I.e. in the code below, class FirstClass is able to invoke function firstNonNull() because the function is annotated with #NonCPS; without the annotation, this code generated the same error as in the question above:
// src/org/example/FirstClass.groovy
package org.example
public class FirstClass implements Serializable {
def steps
def var
FirstClass(steps) {
this.steps = steps
this.var = steps.utils.firstNonNull( [null, null, "assigned_from_ctor"] )
}
}
// vars/utils.groovy
#NonCPS
def firstNonNull( arr ) {
for ( def i in arr ) { if ( i ) { return i } }
return null
}
#Library('my-library')
import org.example.FirstClass
pipeline {
agent any
stages {
stage("foo") {
steps {
script {
first_class = new org.example.FirstClass(this)
}
}
}
}
}
Why does this approach not work with the Report class invoking readJSON?
In my app I use spock in version 2.0-M1-groovy-2.5. I have a problem that even though I mokced a class I get npe from the internals of mocked method.
I have simple class called ReactiveRestHighLevelClient which looks like this
open class ReactiveRestHighLevelClient(val restHighLevelClient: RestHighLevelClient, private val objectMapper: ObjectMapper) {
...
fun index(indexRequest: IndexRequest): Mono<IndexResponse> {
return Mono.create<IndexResponse> { sink ->
restHighLevelClient.indexAsync( // < -- HERE I GET THE NPE EVEN THOUGH THE METHOD IS MOCKED
indexRequest,
object : ActionListener<IndexResponse> {
override fun onFailure(e: Exception) {
e.printStackTrace()
sink.error(e)
}
override fun onResponse(response: IndexResponse) {
sink.success(response)
}
}
)
}
}
...
}
I have class ThreadModelElasticsearchService which uses the ReactiveRestHighLevelClient and looks like this:
#Component
class ThreadModelElasticsearchService(
private val objectMapper: ObjectMapper,
private val reactiveElasticsearchClient: ReactiveRestHighLevelClient,
private val extraFactsService: ExtraFactsService,
private val customerDataService: CustomerDataService,
rateLimiterRegistry: RateLimiterRegistry
) {
...
fun save(operationId: String, threadModel: ThreadModel): Mono<ThreadModel> {
val id = threadModel.threadId
?: ThreadModel.createKey(threadModel.partnerId, threadModel.customerId)
return reactiveElasticsearchClient
.index(
IndexRequest(ThreadModel.INDEX, ThreadModel.TYPE, id)
.source(objectMapper.writeValueAsString(threadModel), XContentType.JSON)
)
.thenReturn(threadModel)
.doOnNext { logger.info("[operation_id=$operationId] -- Saved : $it") }
.onErrorMap { e ->
logger.error("[operation_id=$operationId] -- Error calling elasticSearch", e)
ElasticRepositoryError(e)
}
.rateLimit(elasticRateLimiter)
}
...
}
Finally I have my test class called ThreadModelElasticsearchServiceTest which looks like this:
class ThreadModelElasticsearchServiceTest extends Specification {
ReactiveRestHighLevelClient reactiveElasticsearchClient = Mock()
... other mocks
ThreadModelElasticsearchService threadModelReactiveElasticsearchClient = new
ThreadModelElasticsearchService(objectMapper, reactiveElasticsearchClient, extraFactsService, customerDataService, RateLimiterRegistry.of(RateLimiterConfig.ofDefaults()))
def "should work"() {
given:
ThreadModel threadModel = new ThreadModel(...)
reactiveElasticsearchClient.index(_ as IndexRequest) >> Mono.just(indexResponse)
expect:
threadModelReactiveElasticsearchClient.save("should-work", threadModel).block()
}
When I run the tests I get execption like
java.lang.NullPointerException: null
at com.cb.elastic.search.api.elasticsearch.ReactiveRestHighLevelClient$index$1.accept(ReactiveRestHighLevelClient.kt:76)
which points to the body of the index method of the ReactiveRestHighLevelClient mock which is strange.
How to solve this ?
I am trying to write unit test for addition of 2 numbers which is calling Service class add method() and there is one more class HelperAdd which is called from add() method
Here in unit test case my expected result is different from what is hardcoded in unit test class, How to solve this. Am i doing something wrong in code.
Unit Test Class--
class ControllerTest extends Specification {
Service src
HelperAdd hd
def setup() {
hd = Mock()
src = new Service(
hd: hd
)
}
def "Add 2 numbers"() {
AddModel request = new AddModel()
given: "input"
request.setA(2)
request.setB(3)
when:
src.add(request)
then:
1 * hd.add(_) >> 2
expect:"4"
}
}
Service class --
class Service {
#Autowired
HelperAdd hd
#PostMapping(value="/getdocuments")
def add(#RequestBody AddModel request) {
int a = request.a
int b = request.b
int d = hd.add(a)
int c = a+d
return c
}
}
HelperAdd class--
class HelperAdd {
def add(int a)
{
int k = a+4
return k
}
}
Model class --
#Canonical
class AddModel {
int a
int b
}
I do not understand your problem. The test passes. Your test syntax is weird, though. How about this?
def "Add 2 numbers"() {
given: "input"
def request = new AddModel(a: 2, b: 3)
when:
def result = src.add(request)
then:
1 * hd.add(_) >> 2
result == 4
}
I have a DSL that includes blocks that need to be wrapped as methods returned inside an anonymous class created by the generated code. For example:
model {
task {
val x = 2*5;
Math.pow(2, x)
}
}
should compile to (note task becoming an instance of Runnable, with the body of the task becoming the body of the Runnable.run() method):
import java.util.Collection;
#SuppressWarnings("all")
public class MyFile {
public Collection<Runnable> tasks() {
ArrayList<Runnable> tasks = new ArrayList<>();
tasks.add(getTask0());
return tasks;
}
public static Runnable getTask0() {
Runnable _runnable = new Runnable() {
public void run() {
final int x = (2 * 5);
Math.pow(2, x);
}
}
return _runnable;
}
}
Following the discussion in this question, I was able to get this particular example to work. (Github repo includes unit tests.) But I had to do it by representing the Task element in the grammar as a sequence of XExpressions (source), which my XbaseCompiler subclass had to iterate over (source).
Instead, it would have been nice to be able to just have Task contain an XBlockExpression in a property action, and then in the compiler just do doInternalToJavaStatement(expr.action, it, isReferenced). My sense is that this is really the "right" solution in my case, but when I tried it, this would result in an empty body of the generated run method, as if the block was not processed at all. What's going on, and am I missing some required bits of setup/wiring things together/bindings that are necessary for this to work?
you ususally try to avoid that by using a better inference strategy e.g.
Grammar
Model:
{Model}"model" "{"
vars+=Variable*
tasks+=Task*
"}"
;
Variable:
"var" name=ID ":" type=JvmParameterizedTypeReference
;
Task:
{Task} "task" content=XBlockExpression
;
Inferrer
class MyDslJvmModelInferrer extends AbstractModelInferrer {
#Inject extension JvmTypesBuilder
def dispatch void infer(Model element, IJvmDeclaredTypeAcceptor acceptor, boolean isPreIndexingPhase) {
acceptor.accept(element.toClass("test.Model2")) [
for (v : element.vars) {
members+=v.toField(v.name, v.type.cloneWithProxies) [
]
}
var i = 0;
for (t : element.tasks) {
val doRunName = "doRun"+i
members += t.toMethod("task"+i, Runnable.typeRef()) [
body = '''
return new «Runnable» () {
public void run() {
«doRunName»();
}
};
'''
]
members += t.toMethod(doRunName, Void.TYPE.typeRef()) [
body = t.content
]
i = i + 1
}
]
}
}
and that basically is it.
you may follow https://bugs.eclipse.org/bugs/show_bug.cgi?id=481992
If you really want to adapt the xbase typesystem that may be a lot more of work e.g. (just covering a minimal case)
Grammar
Model:
{Model}"model" "{"
vars+=Variable*
tasks+=Task*
"}"
;
Variable:
"var" name=ID ":" type=JvmParameterizedTypeReference
;
Task:
{Task} "task" content=XTaskContent
;
XTaskContent returns xbase::XExpression:
{XTaskContent} block=XBlockExpression
;
Inferrer
class MyDslJvmModelInferrer extends AbstractModelInferrer {
#Inject extension JvmTypesBuilder
def dispatch void infer(Model element, IJvmDeclaredTypeAcceptor acceptor, boolean isPreIndexingPhase) {
acceptor.accept(element.toClass("test.Model")) [
for (v : element.vars) {
members+=v.toField(v.name, v.type.cloneWithProxies) [
]
}
var i = 0;
for (t : element.tasks) {
members += t.toMethod("task"+i, Runnable.typeRef()) [
body = t.content
]
i = i + 1
}
]
}
}
Type Computer
class MyDslTypeComputer extends XbaseTypeComputer {
override computeTypes(XExpression expression, ITypeComputationState state) {
if (expression instanceof XTaskContent) {
_computeTypes(expression as XTaskContent, state);
} else {
super.computeTypes(expression, state)
}
}
protected def void _computeTypes(XTaskContent object, ITypeComputationState state) {
state.withExpectation(getPrimitiveVoid(state)).computeTypes(object.block)
state.acceptActualType(getTypeForName(Runnable, state), ConformanceFlags.CHECKED_SUCCESS )
}
}
Compiler
class MyDslCompiler extends XbaseCompiler {
override protected internalToConvertedExpression(XExpression obj, ITreeAppendable appendable) {
if (obj instanceof XTaskContent) {
appendable.append("new ").append(Runnable).append("() {").newLine
appendable.increaseIndentation
appendable.append("public void run()").newLine
reassignThisInClosure(appendable, null)
internalToJavaStatement(obj.block, appendable, false)
appendable.newLine
appendable.decreaseIndentation
appendable.newLine.append("}")
} else {
super.internalToConvertedExpression(obj, appendable)
}
}
}
Bindings
class MyDslRuntimeModule extends AbstractMyDslRuntimeModule {
def Class<? extends ITypeComputer> bindITypeComputer() {
return MyDslTypeComputer
}
def Class<? extends XbaseCompiler> bindXbaseCompiler() {
return MyDslCompiler
}
}