Skip to content

Commit 03e243a

Browse files
committed
Fix off-by-one regression in AbstractMethodMockingControl
Issue: SPR-11385, SPR-10885 (cherry picked from commits 69a89b1 and 3a89bc4)
1 parent 60b24cf commit 03e243a

File tree

6 files changed

+290
-307
lines changed

6 files changed

+290
-307
lines changed

spring-aspects/src/main/java/org/springframework/mock/staticmock/AbstractMethodMockingControl.aj

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,41 +18,57 @@ package org.springframework.mock.staticmock;
1818

1919
import java.util.Arrays;
2020
import java.util.LinkedList;
21-
import java.util.List;
21+
22+
import org.springframework.util.ObjectUtils;
2223

2324
/**
2425
* Abstract aspect to enable mocking of methods picked out by a pointcut.
25-
* Sub-aspects must define the mockStaticsTestMethod() pointcut to
26-
* indicate call stacks when mocking should be triggered, and the
27-
* methodToMock() pointcut to pick out a method invocations to mock.
26+
*
27+
* <p>Sub-aspects must define:
28+
* <ul>
29+
* <li>the {@link #mockStaticsTestMethod()} pointcut to indicate call stacks
30+
* when mocking should be triggered
31+
* <li>the {@link #methodToMock()} pointcut to pick out method invocations to mock
32+
* </ul>
2833
*
2934
* @author Rod Johnson
3035
* @author Ramnivas Laddad
36+
* @author Sam Brannen
3137
*/
3238
public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMethod()) {
3339

3440
protected abstract pointcut mockStaticsTestMethod();
3541

3642
protected abstract pointcut methodToMock();
3743

44+
3845
private boolean recording = true;
3946

40-
static enum CallResponse { nothing, return_, throw_ };
4147

42-
// Represents a list of expected calls to static entity methods
48+
static enum CallResponse {
49+
nothing, return_, throw_
50+
};
51+
52+
/**
53+
* Represents a list of expected calls to methods.
54+
*/
4355
// Public to allow inserted code to access: is this normal??
4456
public class Expectations {
4557

46-
// Represents an expected call to a static entity method
58+
/**
59+
* Represents an expected call to a method.
60+
*/
4761
private class Call {
62+
4863
private final String signature;
4964
private final Object[] args;
5065

5166
private Object responseObject; // return value or throwable
5267
private CallResponse responseType = CallResponse.nothing;
5368

54-
public Call(String name, Object[] args) {
55-
this.signature = name;
69+
70+
public Call(String signature, Object[] args) {
71+
this.signature = signature;
5672
this.args = args;
5773
}
5874

@@ -77,7 +93,7 @@ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMeth
7793

7894
public Object throwException(String lastSig, Object[] args) {
7995
checkSignature(lastSig, args);
80-
throw (RuntimeException)responseObject;
96+
throw (RuntimeException) responseObject;
8197
}
8298

8399
private void checkSignature(String lastSig, Object[] args) {
@@ -88,78 +104,94 @@ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMeth
88104
throw new IllegalArgumentException("Arguments don't match");
89105
}
90106
}
107+
108+
@Override
109+
public String toString() {
110+
return String.format("Call with signature [%s] and arguments %s", this.signature,
111+
ObjectUtils.nullSafeToString(args));
112+
}
91113
}
92114

93-
private List<Call> calls = new LinkedList<Call>();
94115

95-
// Calls already verified
116+
/**
117+
* The list of recorded calls.
118+
*/
119+
private final LinkedList<Call> calls = new LinkedList<Call>();
120+
121+
/**
122+
* The number of calls already verified.
123+
*/
96124
private int verified;
97125

126+
98127
public void verify() {
99128
if (verified != calls.size()) {
100-
throw new IllegalStateException("Expected " + calls.size() + " calls, received " + verified);
129+
throw new IllegalStateException("Expected " + calls.size() + " calls, but received " + verified);
101130
}
102131
}
103132

104133
/**
105134
* Validate the call and provide the expected return value.
106135
*/
107136
public Object respond(String lastSig, Object[] args) {
108-
Call call = nextCall();
109-
CallResponse responseType = call.responseType;
110-
if (responseType == CallResponse.return_) {
111-
return call.returnValue(lastSig, args);
112-
}
113-
else if (responseType == CallResponse.throw_) {
114-
return call.throwException(lastSig, args);
115-
}
116-
else if (responseType == CallResponse.nothing) {
117-
// do nothing
137+
Call c = nextCall();
138+
139+
switch (c.responseType) {
140+
case return_: {
141+
return c.returnValue(lastSig, args);
142+
}
143+
case throw_: {
144+
return c.throwException(lastSig, args);
145+
}
146+
default: {
147+
throw new IllegalStateException("Behavior of " + c + " not specified");
148+
}
118149
}
119-
throw new IllegalStateException("Behavior of " + call + " not specified");
120150
}
121151

122152
private Call nextCall() {
123153
verified++;
124154
if (verified > calls.size()) {
125-
throw new IllegalStateException("Expected " + calls.size() + " calls, received " + verified);
155+
throw new IllegalStateException("Expected " + calls.size() + " calls, but received " + verified);
126156
}
127-
return calls.get(verified);
157+
// The 'verified' count is 1-based; whereas, 'calls' is 0-based.
158+
return calls.get(verified - 1);
128159
}
129160

130-
public void expectCall(String lastSig, Object lastArgs[]) {
131-
Call call = new Call(lastSig, lastArgs);
132-
calls.add(call);
161+
public void expectCall(String lastSig, Object[] lastArgs) {
162+
calls.add(new Call(lastSig, lastArgs));
133163
}
134164

135165
public boolean hasCalls() {
136166
return !calls.isEmpty();
137167
}
138168

139169
public void expectReturn(Object retVal) {
140-
Call call = calls.get(calls.size() - 1);
141-
if (call.hasResponseSpecified()) {
142-
throw new IllegalStateException("No static method invoked before setting return value");
170+
Call c = calls.getLast();
171+
if (c.hasResponseSpecified()) {
172+
throw new IllegalStateException("No method invoked before setting return value");
143173
}
144-
call.setReturnVal(retVal);
174+
c.setReturnVal(retVal);
145175
}
146176

147177
public void expectThrow(Throwable throwable) {
148-
Call call = calls.get(calls.size() - 1);
149-
if (call.hasResponseSpecified()) {
150-
throw new IllegalStateException("No static method invoked before setting throwable");
178+
Call c = calls.getLast();
179+
if (c.hasResponseSpecified()) {
180+
throw new IllegalStateException("No method invoked before setting throwable");
151181
}
152-
call.setThrow(throwable);
182+
c.setThrow(throwable);
153183
}
154184
}
155185

156-
private Expectations expectations = new Expectations();
186+
187+
private final Expectations expectations = new Expectations();
188+
157189

158190
after() returning : mockStaticsTestMethod() {
159191
if (recording && (expectations.hasCalls())) {
160192
throw new IllegalStateException(
161-
"Calls recorded, yet playback state never reached: Create expectations then call "
162-
+ this.getClass().getSimpleName() + ".playback()");
193+
"Calls recorded, yet playback state never reached: Create expectations then call "
194+
+ this.getClass().getSimpleName() + ".playback()");
163195
}
164196
expectations.verify();
165197
}

spring-aspects/src/main/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControl.aj

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,50 +17,69 @@
1717
package org.springframework.mock.staticmock;
1818

1919
/**
20-
* Annotation-based aspect to use in test build to enable mocking static methods
21-
* on JPA-annotated {@code @Entity} classes, as used by Roo for finders.
20+
* Annotation-based aspect to use in test builds to enable mocking of static methods
21+
* on JPA-annotated {@code @Entity} classes, as used by Spring Roo for so-called
22+
* <em>finder methods</em>.
2223
*
23-
* <p>Mocking will occur in the call stack of any method in a class (typically a test class)
24-
* that is annotated with the @MockStaticEntityMethods annotation.
24+
* <p>Mocking will occur within the call stack of any method in a class (typically a
25+
* test class) that is annotated with {@code @MockStaticEntityMethods}.
2526
*
26-
* <p>Also provides static methods to simplify the programming model for
27-
* entering playback mode and setting expected return values.
27+
* <p>This aspect also provides static methods to simplify the programming model for
28+
* setting expectations and entering playback mode.
2829
*
2930
* <p>Usage:
3031
* <ol>
31-
* <li>Annotate a test class with @MockStaticEntityMethods.
32-
* <li>In each test method, AnnotationDrivenStaticEntityMockingControl will begin in recording mode.
33-
* Invoke static methods on Entity classes, with each recording-mode invocation
34-
* being followed by an invocation to the static expectReturn() or expectThrow()
35-
* method on AnnotationDrivenStaticEntityMockingControl.
36-
* <li>Invoke the static AnnotationDrivenStaticEntityMockingControl() method.
37-
* <li>Call the code you wish to test that uses the static methods. Verification will
38-
* occur automatically.
32+
* <li>Annotate a test class with {@code @MockStaticEntityMethods}.
33+
* <li>In each test method, {@code AnnotationDrivenStaticEntityMockingControl}
34+
* will begin in <em>recording</em> mode.
35+
* <li>Invoke static methods on JPA-annotated {@code @Entity} classes, with each
36+
* recording-mode invocation being followed by an invocation of either the static
37+
* {@link #expectReturn(Object)} method or the static {@link #expectThrow(Throwable)}
38+
* method on {@code AnnotationDrivenStaticEntityMockingControl}.
39+
* <li>Invoke the static {@link #playback()} method.
40+
* <li>Call the code you wish to test that uses the static methods.
41+
* <li>Verification will occur automatically.
3942
* </ol>
4043
*
4144
* @author Rod Johnson
4245
* @author Ramnivas Laddad
46+
* @author Sam Brannen
4347
* @see MockStaticEntityMethods
4448
*/
4549
public aspect AnnotationDrivenStaticEntityMockingControl extends AbstractMethodMockingControl {
4650

4751
/**
48-
* Stop recording mock calls and enter playback state
52+
* Expect the supplied {@link Object} to be returned by the previous static
53+
* method invocation.
54+
* @see #playback()
4955
*/
50-
public static void playback() {
51-
AnnotationDrivenStaticEntityMockingControl.aspectOf().playbackInternal();
52-
}
53-
5456
public static void expectReturn(Object retVal) {
5557
AnnotationDrivenStaticEntityMockingControl.aspectOf().expectReturnInternal(retVal);
5658
}
5759

60+
/**
61+
* Expect the supplied {@link Throwable} to be thrown by the previous static
62+
* method invocation.
63+
* @see #playback()
64+
*/
5865
public static void expectThrow(Throwable throwable) {
5966
AnnotationDrivenStaticEntityMockingControl.aspectOf().expectThrowInternal(throwable);
6067
}
6168

62-
// Only matches directly annotated @Test methods, to allow methods in
63-
// @MockStatics classes to invoke each other without resetting the mocking environment
69+
/**
70+
* Stop recording mock expectations and enter <em>playback</em> mode.
71+
* @see #expectReturn(Object)
72+
* @see #expectThrow(Throwable)
73+
*/
74+
public static void playback() {
75+
AnnotationDrivenStaticEntityMockingControl.aspectOf().playbackInternal();
76+
}
77+
78+
// Apparently, the following pointcut was originally defined to only match
79+
// methods directly annotated with @Test (in order to allow methods in
80+
// @MockStaticEntityMethods classes to invoke each other without resetting
81+
// the mocking environment); however, this is no longer the case. The current
82+
// pointcut applies to all public methods in @MockStaticEntityMethods classes.
6483
protected pointcut mockStaticsTestMethod() : execution(public * (@MockStaticEntityMethods *).*(..));
6584

6685
protected pointcut methodToMock() : execution(public static * (@javax.persistence.Entity *).*(..));

spring-aspects/src/main/java/org/springframework/mock/staticmock/MockStaticEntityMethods.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,11 +22,13 @@
2222
import java.lang.annotation.Target;
2323

2424
/**
25-
* Annotation to indicate a test class for whose @Test methods
26-
* static methods on Entity classes should be mocked. See
27-
* {@code AbstractMethodMockingControl}.
25+
* Annotation to indicate a test class for whose {@code @Test} methods
26+
* static methods on JPA-annotated {@code @Entity} classes should be mocked.
27+
*
28+
* <p>See {@link AnnotationDrivenStaticEntityMockingControl} for details.
2829
*
2930
* @author Rod Johnson
31+
* @author Sam Brannen
3032
*/
3133
@Retention(RetentionPolicy.RUNTIME)
3234
@Target(ElementType.TYPE)

0 commit comments

Comments
 (0)