Committed by
Gerrit Code Review
Adds manually advancing timer.
Change-Id: I83936f4fff577b0f23404560c6ecfbc0c9f18e2e
Showing
4 changed files
with
802 additions
and
28 deletions
... | @@ -66,24 +66,37 @@ public final class TestUtils { | ... | @@ -66,24 +66,37 @@ public final class TestUtils { |
66 | * | 66 | * |
67 | * @param subject Object where the field belongs | 67 | * @param subject Object where the field belongs |
68 | * @param fieldName name of the field to get | 68 | * @param fieldName name of the field to get |
69 | - * @return value of the field. | ||
70 | * @param <T> subject type | 69 | * @param <T> subject type |
71 | - * @param <U> field value type | 70 | + * @param <U> fieldO value type |
71 | + * @return value of the field. | ||
72 | * @throws TestUtilsException if there are reflection errors while getting | 72 | * @throws TestUtilsException if there are reflection errors while getting |
73 | * the field | 73 | * the field |
74 | */ | 74 | */ |
75 | public static <T, U> U getField(T subject, String fieldName) | 75 | public static <T, U> U getField(T subject, String fieldName) |
76 | throws TestUtilsException { | 76 | throws TestUtilsException { |
77 | try { | 77 | try { |
78 | + NoSuchFieldException exception = null; | ||
78 | @SuppressWarnings("unchecked") | 79 | @SuppressWarnings("unchecked") |
79 | - Class<T> clazz = (Class<T>) subject.getClass(); | 80 | + Class clazz = subject.getClass(); |
81 | + while (clazz != null) { | ||
82 | + try { | ||
80 | Field field = clazz.getDeclaredField(fieldName); | 83 | Field field = clazz.getDeclaredField(fieldName); |
81 | field.setAccessible(true); | 84 | field.setAccessible(true); |
82 | 85 | ||
83 | @SuppressWarnings("unchecked") | 86 | @SuppressWarnings("unchecked") |
84 | U result = (U) field.get(subject); | 87 | U result = (U) field.get(subject); |
85 | return result; | 88 | return result; |
86 | - } catch (NoSuchFieldException | SecurityException | | 89 | + } catch (NoSuchFieldException e) { |
90 | + exception = e; | ||
91 | + if (clazz == clazz.getSuperclass()) { | ||
92 | + break; | ||
93 | + } | ||
94 | + clazz = clazz.getSuperclass(); | ||
95 | + } | ||
96 | + } | ||
97 | + throw new TestUtilsException("Field not found. " + fieldName, exception); | ||
98 | + | ||
99 | + } catch (SecurityException | | ||
87 | IllegalArgumentException | IllegalAccessException e) { | 100 | IllegalArgumentException | IllegalAccessException e) { |
88 | throw new TestUtilsException("getField failed", e); | 101 | throw new TestUtilsException("getField failed", e); |
89 | } | 102 | } | ... | ... |
... | @@ -15,23 +15,24 @@ | ... | @@ -15,23 +15,24 @@ |
15 | */ | 15 | */ |
16 | package org.onlab.util; | 16 | package org.onlab.util; |
17 | 17 | ||
18 | -import org.junit.Ignore; | ||
19 | import org.junit.Test; | 18 | import org.junit.Test; |
20 | 19 | ||
21 | import java.util.List; | 20 | import java.util.List; |
22 | -import java.util.Timer; | ||
23 | import java.util.stream.IntStream; | 21 | import java.util.stream.IntStream; |
24 | 22 | ||
25 | -import static org.junit.Assert.*; | 23 | +import static org.junit.Assert.assertEquals; |
24 | +import static org.junit.Assert.assertFalse; | ||
25 | +import static org.junit.Assert.assertTrue; | ||
26 | import static org.onlab.junit.TestTools.assertAfter; | 26 | import static org.onlab.junit.TestTools.assertAfter; |
27 | -import static org.onlab.junit.TestTools.delay; | ||
28 | 27 | ||
29 | /** | 28 | /** |
30 | * Tests the operation of the accumulator. | 29 | * Tests the operation of the accumulator. |
31 | */ | 30 | */ |
32 | public class AbstractAccumulatorTest { | 31 | public class AbstractAccumulatorTest { |
33 | 32 | ||
34 | - private final Timer timer = new Timer(); | 33 | + |
34 | + private final ManuallyAdvancingTimer timer = new ManuallyAdvancingTimer(); | ||
35 | + | ||
35 | 36 | ||
36 | @Test | 37 | @Test |
37 | public void basics() throws Exception { | 38 | public void basics() throws Exception { |
... | @@ -42,7 +43,6 @@ public class AbstractAccumulatorTest { | ... | @@ -42,7 +43,6 @@ public class AbstractAccumulatorTest { |
42 | assertEquals("incorrect idle ms", 70, accumulator.maxIdleMillis()); | 43 | assertEquals("incorrect idle ms", 70, accumulator.maxIdleMillis()); |
43 | } | 44 | } |
44 | 45 | ||
45 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
46 | @Test | 46 | @Test |
47 | public void eventTrigger() { | 47 | public void eventTrigger() { |
48 | TestAccumulator accumulator = new TestAccumulator(); | 48 | TestAccumulator accumulator = new TestAccumulator(); |
... | @@ -52,43 +52,40 @@ public class AbstractAccumulatorTest { | ... | @@ -52,43 +52,40 @@ public class AbstractAccumulatorTest { |
52 | accumulator.add(new TestItem("d")); | 52 | accumulator.add(new TestItem("d")); |
53 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 53 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
54 | accumulator.add(new TestItem("e")); | 54 | accumulator.add(new TestItem("e")); |
55 | - delay(20); | 55 | + timer.advanceTimeMillis(20, 10); |
56 | assertFalse("should have fired", accumulator.batch.isEmpty()); | 56 | assertFalse("should have fired", accumulator.batch.isEmpty()); |
57 | assertEquals("incorrect batch", "abcde", accumulator.batch); | 57 | assertEquals("incorrect batch", "abcde", accumulator.batch); |
58 | } | 58 | } |
59 | 59 | ||
60 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
61 | @Test | 60 | @Test |
62 | public void timeTrigger() { | 61 | public void timeTrigger() { |
63 | TestAccumulator accumulator = new TestAccumulator(); | 62 | TestAccumulator accumulator = new TestAccumulator(); |
64 | accumulator.add(new TestItem("a")); | 63 | accumulator.add(new TestItem("a")); |
65 | - delay(30); | 64 | + timer.advanceTimeMillis(30, 1); |
66 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 65 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
67 | accumulator.add(new TestItem("b")); | 66 | accumulator.add(new TestItem("b")); |
68 | - delay(30); | 67 | + timer.advanceTimeMillis(30, 1); |
69 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 68 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
70 | accumulator.add(new TestItem("c")); | 69 | accumulator.add(new TestItem("c")); |
71 | - delay(30); | 70 | + timer.advanceTimeMillis(30, 1); |
72 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 71 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
73 | accumulator.add(new TestItem("d")); | 72 | accumulator.add(new TestItem("d")); |
74 | - delay(60); | 73 | + timer.advanceTimeMillis(10, 10); |
75 | assertFalse("should have fired", accumulator.batch.isEmpty()); | 74 | assertFalse("should have fired", accumulator.batch.isEmpty()); |
76 | assertEquals("incorrect batch", "abcd", accumulator.batch); | 75 | assertEquals("incorrect batch", "abcd", accumulator.batch); |
77 | } | 76 | } |
78 | 77 | ||
79 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
80 | @Test | 78 | @Test |
81 | public void idleTrigger() { | 79 | public void idleTrigger() { |
82 | TestAccumulator accumulator = new TestAccumulator(); | 80 | TestAccumulator accumulator = new TestAccumulator(); |
83 | accumulator.add(new TestItem("a")); | 81 | accumulator.add(new TestItem("a")); |
84 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 82 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
85 | accumulator.add(new TestItem("b")); | 83 | accumulator.add(new TestItem("b")); |
86 | - delay(80); | 84 | + timer.advanceTimeMillis(70, 10); |
87 | assertFalse("should have fired", accumulator.batch.isEmpty()); | 85 | assertFalse("should have fired", accumulator.batch.isEmpty()); |
88 | assertEquals("incorrect batch", "ab", accumulator.batch); | 86 | assertEquals("incorrect batch", "ab", accumulator.batch); |
89 | } | 87 | } |
90 | 88 | ||
91 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
92 | @Test | 89 | @Test |
93 | public void readyIdleTrigger() { | 90 | public void readyIdleTrigger() { |
94 | TestAccumulator accumulator = new TestAccumulator(); | 91 | TestAccumulator accumulator = new TestAccumulator(); |
... | @@ -96,30 +93,28 @@ public class AbstractAccumulatorTest { | ... | @@ -96,30 +93,28 @@ public class AbstractAccumulatorTest { |
96 | accumulator.add(new TestItem("a")); | 93 | accumulator.add(new TestItem("a")); |
97 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 94 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
98 | accumulator.add(new TestItem("b")); | 95 | accumulator.add(new TestItem("b")); |
99 | - delay(80); | 96 | + timer.advanceTimeMillis(80, 1); |
100 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 97 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
101 | accumulator.ready = true; | 98 | accumulator.ready = true; |
102 | - delay(80); | 99 | + timer.advanceTimeMillis(80, 10); |
103 | assertFalse("should have fired", accumulator.batch.isEmpty()); | 100 | assertFalse("should have fired", accumulator.batch.isEmpty()); |
104 | assertEquals("incorrect batch", "ab", accumulator.batch); | 101 | assertEquals("incorrect batch", "ab", accumulator.batch); |
105 | } | 102 | } |
106 | 103 | ||
107 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
108 | @Test | 104 | @Test |
109 | public void readyLongTrigger() { | 105 | public void readyLongTrigger() { |
110 | TestAccumulator accumulator = new TestAccumulator(); | 106 | TestAccumulator accumulator = new TestAccumulator(); |
111 | accumulator.ready = false; | 107 | accumulator.ready = false; |
112 | - delay(120); | 108 | + timer.advanceTimeMillis(120, 1); |
113 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 109 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
114 | accumulator.add(new TestItem("a")); | 110 | accumulator.add(new TestItem("a")); |
115 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 111 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
116 | accumulator.ready = true; | 112 | accumulator.ready = true; |
117 | - delay(80); | 113 | + timer.advanceTimeMillis(120, 10); |
118 | assertFalse("should have fired", accumulator.batch.isEmpty()); | 114 | assertFalse("should have fired", accumulator.batch.isEmpty()); |
119 | assertEquals("incorrect batch", "a", accumulator.batch); | 115 | assertEquals("incorrect batch", "a", accumulator.batch); |
120 | } | 116 | } |
121 | 117 | ||
122 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
123 | @Test | 118 | @Test |
124 | public void readyMaxTrigger() { | 119 | public void readyMaxTrigger() { |
125 | TestAccumulator accumulator = new TestAccumulator(); | 120 | TestAccumulator accumulator = new TestAccumulator(); |
... | @@ -133,16 +128,16 @@ public class AbstractAccumulatorTest { | ... | @@ -133,16 +128,16 @@ public class AbstractAccumulatorTest { |
133 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); | 128 | assertTrue("should not have fired yet", accumulator.batch.isEmpty()); |
134 | accumulator.ready = true; | 129 | accumulator.ready = true; |
135 | accumulator.add(new TestItem("g")); | 130 | accumulator.add(new TestItem("g")); |
136 | - delay(5); | 131 | + timer.advanceTimeMillis(10, 10); |
137 | assertFalse("should have fired", accumulator.batch.isEmpty()); | 132 | assertFalse("should have fired", accumulator.batch.isEmpty()); |
138 | assertEquals("incorrect batch", "abcdefg", accumulator.batch); | 133 | assertEquals("incorrect batch", "abcdefg", accumulator.batch); |
139 | } | 134 | } |
140 | 135 | ||
141 | - @Ignore("FIXME: timing sensitive test failing randomly.") | ||
142 | @Test | 136 | @Test |
143 | public void stormTest() { | 137 | public void stormTest() { |
144 | TestAccumulator accumulator = new TestAccumulator(); | 138 | TestAccumulator accumulator = new TestAccumulator(); |
145 | IntStream.range(0, 1000).forEach(i -> accumulator.add(new TestItem("#" + i))); | 139 | IntStream.range(0, 1000).forEach(i -> accumulator.add(new TestItem("#" + i))); |
140 | + timer.advanceTimeMillis(1); | ||
146 | assertAfter(100, () -> assertEquals("wrong item count", 1000, accumulator.itemCount)); | 141 | assertAfter(100, () -> assertEquals("wrong item count", 1000, accumulator.itemCount)); |
147 | assertEquals("wrong batch count", 200, accumulator.batchCount); | 142 | assertEquals("wrong batch count", 200, accumulator.batchCount); |
148 | } | 143 | } |
... | @@ -180,5 +175,4 @@ public class AbstractAccumulatorTest { | ... | @@ -180,5 +175,4 @@ public class AbstractAccumulatorTest { |
180 | return ready; | 175 | return ready; |
181 | } | 176 | } |
182 | } | 177 | } |
183 | - | ||
184 | } | 178 | } | ... | ... |
1 | +/* | ||
2 | + * Copyright 2015 Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +package org.onlab.util; | ||
18 | + | ||
19 | +import com.google.common.collect.Lists; | ||
20 | +import org.onlab.junit.TestUtils; | ||
21 | +import org.slf4j.Logger; | ||
22 | + | ||
23 | +import java.util.Date; | ||
24 | +import java.util.Iterator; | ||
25 | +import java.util.LinkedList; | ||
26 | +import java.util.TimerTask; | ||
27 | +import java.util.concurrent.ExecutorService; | ||
28 | +import java.util.concurrent.Executors; | ||
29 | + | ||
30 | +import static com.google.common.base.Preconditions.checkNotNull; | ||
31 | +import static org.onlab.junit.TestTools.delay; | ||
32 | +import static org.slf4j.LoggerFactory.getLogger; | ||
33 | + | ||
34 | + | ||
35 | +/** | ||
36 | + * Provides manually scheduled timer utility. All schedulable methods are subject to overflow (you can set a period of | ||
37 | + * max long). Additionally if a skip skips a period of time greater than one period for a periodic task that task will | ||
38 | + * only be executed once for that skip and scheduled it's period after the last execution. | ||
39 | + */ | ||
40 | +public class ManuallyAdvancingTimer extends java.util.Timer { | ||
41 | + | ||
42 | + /* States whether or not the static values from timer task have been set ensures population will only occur once.*/ | ||
43 | + private boolean staticsPopulated = false; | ||
44 | + | ||
45 | + /* Virgin value from timer task */ | ||
46 | + private int virginState; | ||
47 | + | ||
48 | + /* Scheduled value from timer task */ | ||
49 | + private int scheduledState; | ||
50 | + | ||
51 | + /* Executed value from timer task */ | ||
52 | + private int executedState; | ||
53 | + | ||
54 | + /* Cancelled value from timer task */ | ||
55 | + private int cancelledState; | ||
56 | + | ||
57 | + private final Logger logger = getLogger(getClass()); | ||
58 | + | ||
59 | + /* Service for executing timer tasks */ | ||
60 | + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); | ||
61 | + | ||
62 | + /* Internal time representation independent of system time, manually advanced */ | ||
63 | + private final TimerKeeper timerKeeper = new TimerKeeper(); | ||
64 | + | ||
65 | + /* Data structure for tracking tasks */ | ||
66 | + private final TaskQueue queue = new TaskQueue(); | ||
67 | + | ||
68 | + @Override | ||
69 | + public void schedule(TimerTask task, long delay) { | ||
70 | + if (!staticsPopulated) { | ||
71 | + populateStatics(task); | ||
72 | + } | ||
73 | + if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay : | ||
74 | + timerKeeper.currentTimeInMillis() - delay, 0)) { | ||
75 | + logger.error("Failed to submit task"); | ||
76 | + } | ||
77 | + } | ||
78 | + | ||
79 | + @Override | ||
80 | + public void schedule(TimerTask task, Date time) { | ||
81 | + if (!staticsPopulated) { | ||
82 | + populateStatics(task); | ||
83 | + } | ||
84 | + if (!submitTask(task, time.getTime(), 0)) { | ||
85 | + logger.error("Failed to submit task"); | ||
86 | + } | ||
87 | + } | ||
88 | + | ||
89 | + @Override | ||
90 | + public void schedule(TimerTask task, long delay, long period) { | ||
91 | + if (!staticsPopulated) { | ||
92 | + populateStatics(task); | ||
93 | + } | ||
94 | + if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay : | ||
95 | + timerKeeper.currentTimeInMillis() - delay, period)) { | ||
96 | + logger.error("Failed to submit task"); | ||
97 | + } | ||
98 | + } | ||
99 | + | ||
100 | + @Override | ||
101 | + public void schedule(TimerTask task, Date firstTime, long period) { | ||
102 | + if (!staticsPopulated) { | ||
103 | + populateStatics(task); | ||
104 | + } | ||
105 | + if (!submitTask(task, firstTime.getTime(), period)) { | ||
106 | + logger.error("Failed to submit task"); | ||
107 | + } | ||
108 | + } | ||
109 | + | ||
110 | + /*################################################WARNING################################################*/ | ||
111 | + /* Schedule at fixed rate methods do not work exactly as in the java timer. They are clones of the periodic | ||
112 | + *scheduling methods. */ | ||
113 | + @Override | ||
114 | + public void scheduleAtFixedRate(TimerTask task, long delay, long period) { | ||
115 | + if (!staticsPopulated) { | ||
116 | + populateStatics(task); | ||
117 | + } | ||
118 | + if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay : | ||
119 | + timerKeeper.currentTimeInMillis() - delay, period)) { | ||
120 | + logger.error("Failed to submit task"); | ||
121 | + } | ||
122 | + } | ||
123 | + | ||
124 | + @Override | ||
125 | + public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { | ||
126 | + if (!staticsPopulated) { | ||
127 | + populateStatics(task); | ||
128 | + } | ||
129 | + if (!submitTask(task, firstTime.getTime(), period)) { | ||
130 | + logger.error("Failed to submit task"); | ||
131 | + } | ||
132 | + } | ||
133 | + | ||
134 | + @Override | ||
135 | + public void cancel() { | ||
136 | + executorService.shutdown(); | ||
137 | + queue.clear(); | ||
138 | + } | ||
139 | + | ||
140 | + @Override | ||
141 | + public int purge() { | ||
142 | + return queue.removeCancelled(); | ||
143 | + } | ||
144 | + | ||
145 | + /** | ||
146 | + * Returns the virtual current time in millis. | ||
147 | + * | ||
148 | + * @return long representing simulated current time. | ||
149 | + */ | ||
150 | + public long currentTimeInMillis() { | ||
151 | + return timerKeeper.currentTimeInMillis(); | ||
152 | + } | ||
153 | + | ||
154 | + /** | ||
155 | + * Returns the new simulated current time in millis after advancing the absolute value of millis to advance. | ||
156 | + * Triggers event execution of all events scheduled for execution at times up to and including the returned time. | ||
157 | + * Passing in the number zero has no effect. | ||
158 | + * | ||
159 | + * @param millisToAdvance the number of millis to advance. | ||
160 | + * @return a long representing the current simulated time in millis | ||
161 | + */ | ||
162 | + public long advanceTimeMillis(long millisToAdvance) { | ||
163 | + return timerKeeper.advanceTimeMillis(millisToAdvance); | ||
164 | + } | ||
165 | + | ||
166 | + /** | ||
167 | + * Advances the virtual time a certain number of millis triggers execution delays a certain amount to | ||
168 | + * allow time for execution. | ||
169 | + * | ||
170 | + * @param virtualTimeAdvance the time to be advances in millis of simulated time. | ||
171 | + * @param realTimeDelay the time to delay in real time to allow for processing. | ||
172 | + */ | ||
173 | + public void advanceTimeMillis(long virtualTimeAdvance, int realTimeDelay) { | ||
174 | + timerKeeper.advanceTimeMillis(virtualTimeAdvance); | ||
175 | + delay(realTimeDelay); | ||
176 | + } | ||
177 | + | ||
178 | + /** | ||
179 | + * Sets up the task and submits it to the queue. | ||
180 | + * | ||
181 | + * @param task the task to be added to the queue | ||
182 | + * @param runtime the first runtime of the task | ||
183 | + * @param period the period between runs thereafter | ||
184 | + * @return returns true if the task was successfully submitted, false otherwise | ||
185 | + */ | ||
186 | + private boolean submitTask(TimerTask task, long runtime, long period) { | ||
187 | + checkNotNull(task); | ||
188 | + try { | ||
189 | + TestUtils.setField(task, "state", scheduledState); | ||
190 | + TestUtils.setField(task, "nextExecutionTime", runtime); | ||
191 | + TestUtils.setField(task, "period", period); | ||
192 | + } catch (TestUtils.TestUtilsException e) { | ||
193 | + e.printStackTrace(); | ||
194 | + return false; | ||
195 | + } | ||
196 | + queue.insertOrdered(task); | ||
197 | + return true; | ||
198 | + } | ||
199 | + | ||
200 | + /** | ||
201 | + * Executes the given task (only if it is in the scheduled state) and proceeds to reschedule it or mark it as | ||
202 | + * executed. Does not remove from the queue (this must be done outside). | ||
203 | + * | ||
204 | + * @param task the timer task to be executed | ||
205 | + */ | ||
206 | + private boolean executeTask(TimerTask task) { | ||
207 | + checkNotNull(task); | ||
208 | + int currentState; | ||
209 | + try { | ||
210 | + currentState = TestUtils.getField(task, "state"); | ||
211 | + } catch (TestUtils.TestUtilsException e) { | ||
212 | + logger.error("Could not get state of task."); | ||
213 | + e.printStackTrace(); | ||
214 | + return false; | ||
215 | + } | ||
216 | + //If cancelled or already executed stop here. | ||
217 | + if (currentState == executedState || currentState == cancelledState) { | ||
218 | + return false; | ||
219 | + } else if (currentState == virginState) { | ||
220 | + logger.error("Task was set for execution without being scheduled."); | ||
221 | + return false; | ||
222 | + } else if (currentState == scheduledState) { | ||
223 | + long period; | ||
224 | + | ||
225 | + try { | ||
226 | + period = TestUtils.getField(task, "period"); | ||
227 | + } catch (TestUtils.TestUtilsException e) { | ||
228 | + logger.error("Could not read period of task."); | ||
229 | + e.printStackTrace(); | ||
230 | + return false; | ||
231 | + } | ||
232 | + //Period of zero means one time execution. | ||
233 | + if (period == 0) { | ||
234 | + try { | ||
235 | + TestUtils.setField(task, "state", executedState); | ||
236 | + } catch (TestUtils.TestUtilsException e) { | ||
237 | + logger.error("Could not set executed state."); | ||
238 | + e.printStackTrace(); | ||
239 | + return false; | ||
240 | + } | ||
241 | + executorService.execute(task); | ||
242 | + return true; | ||
243 | + } else { | ||
244 | + //Calculate next execution time, using absolute value of period | ||
245 | + long nextTime = (period > 0) ? (timerKeeper.currentTimeInMillis() + period) : | ||
246 | + (timerKeeper.currentTimeInMillis() - period); | ||
247 | + try { | ||
248 | + TestUtils.setField(task, "nextExecutionTime", nextTime); | ||
249 | + } catch (TestUtils.TestUtilsException e) { | ||
250 | + logger.error("Could not set next execution time."); | ||
251 | + e.printStackTrace(); | ||
252 | + return false; | ||
253 | + } | ||
254 | + //Schedule next execution | ||
255 | + queue.insertOrdered(task); | ||
256 | + executorService.execute(task); | ||
257 | + return true; | ||
258 | + } | ||
259 | + } | ||
260 | + logger.error("State property of {} is in an illegal state and did not execute.", task); | ||
261 | + return false; | ||
262 | + } | ||
263 | + | ||
264 | + /** | ||
265 | + * Executes all tasks in the queue scheduled for execution up to and including the current time. | ||
266 | + * | ||
267 | + * @return the total number of tasks run, -1 if failure | ||
268 | + */ | ||
269 | + private int executeEventsUpToPresent() { | ||
270 | + int totalRun = 0; | ||
271 | + if (queue.isEmpty()) { | ||
272 | + return -1; | ||
273 | + } | ||
274 | + TimerTask currTask = queue.peek(); | ||
275 | + long currExecTime; | ||
276 | + try { | ||
277 | + currExecTime = TestUtils.getField(currTask, "nextExecutionTime"); | ||
278 | + } catch (TestUtils.TestUtilsException e) { | ||
279 | + e.printStackTrace(); | ||
280 | + throw new RuntimeException("Could not get nextExecutionTime"); | ||
281 | + } | ||
282 | + while (currExecTime <= timerKeeper.currentTimeInMillis()) { | ||
283 | + if (executeTask(queue.pop())) { | ||
284 | + totalRun++; | ||
285 | + } | ||
286 | + if (queue.isEmpty()) { | ||
287 | + break; | ||
288 | + } | ||
289 | + currTask = queue.peek(); | ||
290 | + try { | ||
291 | + currExecTime = TestUtils.getField(currTask, "nextExecutionTime"); | ||
292 | + } catch (TestUtils.TestUtilsException e) { | ||
293 | + e.printStackTrace(); | ||
294 | + throw new RuntimeException("Could not get nextExecutionTime"); | ||
295 | + } | ||
296 | + } | ||
297 | + return totalRun; | ||
298 | + } | ||
299 | + | ||
300 | + /** | ||
301 | + * Populates the static fields from timer task. Should only be called once. | ||
302 | + */ | ||
303 | + private void populateStatics(TimerTask task) { | ||
304 | + try { | ||
305 | + virginState = TestUtils.getField(task, "VIRGIN"); | ||
306 | + scheduledState = TestUtils.getField(task, "SCHEDULED"); | ||
307 | + executedState = TestUtils.getField(task, "EXECUTED"); | ||
308 | + cancelledState = TestUtils.getField(task, "CANCELLED"); | ||
309 | + staticsPopulated = true; | ||
310 | + } catch (TestUtils.TestUtilsException e) { | ||
311 | + e.printStackTrace(); | ||
312 | + } | ||
313 | + } | ||
314 | + | ||
315 | + /** | ||
316 | + * A class used to maintain the virtual time. | ||
317 | + */ | ||
318 | + private class TimerKeeper { | ||
319 | + | ||
320 | + private long currentTime = 0; | ||
321 | + | ||
322 | + /** | ||
323 | + * Returns the virtual current time in millis. | ||
324 | + * | ||
325 | + * @return long representing simulated current time. | ||
326 | + */ | ||
327 | + long currentTimeInMillis() { | ||
328 | + return currentTime; | ||
329 | + } | ||
330 | + | ||
331 | + /** | ||
332 | + * Returns the new simulated current time in millis after advancing the absolute value of millis to advance. | ||
333 | + * Triggers event execution of all events scheduled for execution at times up to and including the returned | ||
334 | + * time. Passing in the number zero has no effect. | ||
335 | + * | ||
336 | + * @param millisToAdvance the number of millis to advance. | ||
337 | + * @return a long representing the current simulated time in millis | ||
338 | + */ | ||
339 | + long advanceTimeMillis(long millisToAdvance) { | ||
340 | + currentTime = (millisToAdvance >= 0) ? (currentTime + millisToAdvance) : (currentTime - millisToAdvance); | ||
341 | + if (millisToAdvance != 0) { | ||
342 | + executeEventsUpToPresent(); | ||
343 | + } | ||
344 | + return currentTime; | ||
345 | + } | ||
346 | + } | ||
347 | + | ||
348 | + /** | ||
349 | + * A queue backed by a linked list. Keeps elements sorted in ascending order of execution time. All calls are safe | ||
350 | + * even on empty queue's. | ||
351 | + */ | ||
352 | + private class TaskQueue { | ||
353 | + private final LinkedList<TimerTask> taskList = Lists.newLinkedList(); | ||
354 | + | ||
355 | + /** | ||
356 | + * Adds the task to the queue in ascending order of scheduled execution. If execution time has already passed | ||
357 | + * execute immediately. | ||
358 | + * | ||
359 | + * @param task the task to be added to the queue | ||
360 | + */ | ||
361 | + void insertOrdered(TimerTask task) { | ||
362 | + //Using O(N) insertion because random access is expensive in linked lists worst case is 2N links followed | ||
363 | + // for binary insertion vs N for simple insertion. | ||
364 | + checkNotNull(task); | ||
365 | + if (!staticsPopulated) { | ||
366 | + populateStatics(task); | ||
367 | + } | ||
368 | + long insertTime; | ||
369 | + try { | ||
370 | + insertTime = TestUtils.getField(task, "nextExecutionTime"); | ||
371 | + TestUtils.setField(task, "state", scheduledState); | ||
372 | + } catch (TestUtils.TestUtilsException e) { | ||
373 | + e.printStackTrace(); | ||
374 | + return; | ||
375 | + } | ||
376 | + //If the task was scheduled in the past or for the current time run it immediately and do not add to the | ||
377 | + // queue, subsequent executions will be scheduled as normal | ||
378 | + if (insertTime <= timerKeeper.currentTimeInMillis()) { | ||
379 | + executeTask(task); | ||
380 | + return; | ||
381 | + } | ||
382 | + | ||
383 | + Iterator<TimerTask> iter = taskList.iterator(); | ||
384 | + int positionCounter = 0; | ||
385 | + long nextTaskTime; | ||
386 | + TimerTask currentTask; | ||
387 | + while (iter.hasNext()) { | ||
388 | + currentTask = iter.next(); | ||
389 | + try { | ||
390 | + nextTaskTime = TestUtils.getField(currentTask, "nextExecutionTime"); | ||
391 | + } catch (TestUtils.TestUtilsException e) { | ||
392 | + e.printStackTrace(); | ||
393 | + return; | ||
394 | + } | ||
395 | + if (insertTime < nextTaskTime) { | ||
396 | + taskList.add(positionCounter, task); | ||
397 | + return; | ||
398 | + } | ||
399 | + positionCounter++; | ||
400 | + } | ||
401 | + taskList.addLast(task); | ||
402 | + } | ||
403 | + | ||
404 | + /** | ||
405 | + * Returns the first item in the queue (next scheduled for execution) without removing it, returns null if the | ||
406 | + * queue is empty. | ||
407 | + * | ||
408 | + * @return the next TimerTask to run or null if the queue is empty | ||
409 | + */ | ||
410 | + TimerTask peek() { | ||
411 | + if (taskList.isEmpty()) { | ||
412 | + return null; | ||
413 | + } | ||
414 | + return taskList.getFirst(); | ||
415 | + } | ||
416 | + | ||
417 | + /** | ||
418 | + * Returns and removes the first item in the queue or null if it is empty. | ||
419 | + * | ||
420 | + * @return the first element of the queue or null if the queue is empty | ||
421 | + */ | ||
422 | + TimerTask pop() { | ||
423 | + if (taskList.isEmpty()) { | ||
424 | + return null; | ||
425 | + } | ||
426 | + return taskList.pop(); | ||
427 | + } | ||
428 | + | ||
429 | + /** | ||
430 | + * Performs a sort on the set of timer tasks, earliest task is first. Does nothing if queue is empty. | ||
431 | + */ | ||
432 | + void sort() { | ||
433 | + if (taskList.isEmpty()) { | ||
434 | + return; | ||
435 | + } | ||
436 | + taskList.sort((o1, o2) -> { | ||
437 | + checkNotNull(o1); | ||
438 | + checkNotNull(o2); | ||
439 | + long executionTimeOne; | ||
440 | + long executionTimeTwo; | ||
441 | + try { | ||
442 | + executionTimeOne = TestUtils.getField(o1, "nextExecutionTime"); | ||
443 | + executionTimeTwo = TestUtils.getField(o2, "nextExecutionTime"); | ||
444 | + } catch (TestUtils.TestUtilsException e) { | ||
445 | + e.printStackTrace(); | ||
446 | + throw new RuntimeException("Could not get next execution time."); | ||
447 | + } | ||
448 | + if (executionTimeOne == executionTimeTwo) { | ||
449 | + return 0; | ||
450 | + } else if (executionTimeOne < executionTimeTwo) { | ||
451 | + return -1; | ||
452 | + } else { | ||
453 | + return 1; | ||
454 | + } | ||
455 | + }); | ||
456 | + } | ||
457 | + | ||
458 | + /** | ||
459 | + * Returns whether the queue is currently empty. | ||
460 | + * | ||
461 | + * @return true if the queue is empty, false otherwise | ||
462 | + */ | ||
463 | + boolean isEmpty() { | ||
464 | + return taskList.isEmpty(); | ||
465 | + } | ||
466 | + | ||
467 | + /** | ||
468 | + * Clears the underlying list of the queue. | ||
469 | + */ | ||
470 | + void clear() { | ||
471 | + taskList.clear(); | ||
472 | + } | ||
473 | + | ||
474 | + /** | ||
475 | + * Removes all cancelled tasks from the queue. Has no effect on behavior. | ||
476 | + * | ||
477 | + * @return returns the total number of items removed, -1 if list is empty or failure occurs. | ||
478 | + */ | ||
479 | + int removeCancelled() { | ||
480 | + if (taskList.isEmpty()) { | ||
481 | + return -1; | ||
482 | + } | ||
483 | + int removedCount = 0; | ||
484 | + Iterator<TimerTask> taskIterator = taskList.iterator(); | ||
485 | + TimerTask currTask; | ||
486 | + int currState; | ||
487 | + while (taskIterator.hasNext()) { | ||
488 | + currTask = taskIterator.next(); | ||
489 | + try { | ||
490 | + currState = TestUtils.getField(currTask, "state"); | ||
491 | + } catch (TestUtils.TestUtilsException e) { | ||
492 | + logger.error("Could not get task state."); | ||
493 | + e.printStackTrace(); | ||
494 | + return -1; | ||
495 | + } | ||
496 | + if (currState == cancelledState) { | ||
497 | + removedCount++; | ||
498 | + taskIterator.remove(); | ||
499 | + } | ||
500 | + } | ||
501 | + return removedCount; | ||
502 | + } | ||
503 | + } | ||
504 | +} |
1 | +/* | ||
2 | + * Copyright 2015 Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +package org.onlab.util; | ||
18 | + | ||
19 | +import com.google.common.collect.Lists; | ||
20 | +import org.junit.Before; | ||
21 | +import org.junit.Test; | ||
22 | + | ||
23 | +import java.util.ArrayList; | ||
24 | +import java.util.Date; | ||
25 | +import java.util.TimerTask; | ||
26 | +import java.util.concurrent.atomic.AtomicInteger; | ||
27 | + | ||
28 | +import static org.junit.Assert.assertEquals; | ||
29 | +import static org.junit.Assert.assertFalse; | ||
30 | +import static org.junit.Assert.assertTrue; | ||
31 | +import static org.onlab.junit.TestTools.delay; | ||
32 | + | ||
33 | +/** | ||
34 | + * Testing class for manually advancing timer. | ||
35 | + */ | ||
36 | +public class ManuallyAdvancingTimerTest { | ||
37 | + | ||
38 | + private ManuallyAdvancingTimer timer; | ||
39 | + | ||
40 | + /* Generates unique id's for TestTasks */ | ||
41 | + private AtomicInteger idGenerator; | ||
42 | + | ||
43 | + /* Tracks TestTasks in order of creation, tasks are automatically added at creation. */ | ||
44 | + private ArrayList<TestTask> taskList; | ||
45 | + | ||
46 | + /* Total number of tasks run */ | ||
47 | + private AtomicInteger tasksRunCount; | ||
48 | + | ||
49 | + // FIXME if this class fails first try increasing the real time delay to account for heavy system load. | ||
50 | + private static final int REAL_TIME_DELAY = 1; | ||
51 | + | ||
52 | + /** | ||
53 | + * Sets up the testing environment. | ||
54 | + */ | ||
55 | + @Before | ||
56 | + public void setup() { | ||
57 | + timer = new ManuallyAdvancingTimer(); | ||
58 | + idGenerator = new AtomicInteger(1); | ||
59 | + tasksRunCount = new AtomicInteger(0); | ||
60 | + taskList = Lists.newArrayList(); | ||
61 | + } | ||
62 | + | ||
63 | + /** | ||
64 | + * Tests the one time schedule with delay. | ||
65 | + * | ||
66 | + * @throws Exception throws an exception if the test fails | ||
67 | + */ | ||
68 | + @Test | ||
69 | + public void testScheduleByDelay() throws Exception { | ||
70 | + /* Test scheduling in the future as normal. */ | ||
71 | + timer.schedule(new TestTask(), 10); | ||
72 | + timer.advanceTimeMillis(5); | ||
73 | + assertFalse(taskList.get(0).hasRun()); | ||
74 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
75 | + assertTrue(taskList.get(0).hasRun()); | ||
76 | + | ||
77 | + /* Test scheduling with negative numbers */ | ||
78 | + timer.schedule(new TestTask(), -10); | ||
79 | + timer.advanceTimeMillis(5); | ||
80 | + assertFalse(taskList.get(1).hasRun()); | ||
81 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
82 | + assertTrue(taskList.get(1).hasRun()); | ||
83 | + | ||
84 | + /* Reset list, counter and timer for next test */ | ||
85 | + taskList.clear(); | ||
86 | + idGenerator.set(1); | ||
87 | + tasksRunCount.set(0); | ||
88 | + | ||
89 | + for (int i = 0; i < 50; i++) { | ||
90 | + timer.schedule(new TestTask(), i); | ||
91 | + } | ||
92 | + /* Test that a task scheduled for present is run and not placed in the queue */ | ||
93 | + assertEquals("Only the first task should have run.", 1, tasksRunCount.get()); | ||
94 | + | ||
95 | + for (int i = 2; i <= 50; i++) { | ||
96 | + timer.advanceTimeMillis(1, REAL_TIME_DELAY); | ||
97 | + assertEquals("One task should be executed per loop", i, tasksRunCount.get()); | ||
98 | + } | ||
99 | + /* Below tests ordered insertion, this will only be done once, it is the same for all schedule methods. */ | ||
100 | + | ||
101 | + tasksRunCount.set(0); | ||
102 | + | ||
103 | + for (int i = 0; i < 10; i++) { | ||
104 | + timer.schedule(new TestTask(), 500); | ||
105 | + } | ||
106 | + | ||
107 | + assertEquals("No new tasks should have been run since run count reset.", 0, tasksRunCount.get()); | ||
108 | + timer.schedule(new TestTask(), 10); | ||
109 | + assertEquals("No new tasks should have been run since run count reset.", 0, tasksRunCount.get()); | ||
110 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
111 | + assertEquals("One new tasks should have been run since run count reset.", 1, tasksRunCount.get()); | ||
112 | + timer.advanceTimeMillis(510, REAL_TIME_DELAY); | ||
113 | + assertEquals("Eleven new tasks should have been run since run count reset.", 11, tasksRunCount.get()); | ||
114 | + } | ||
115 | + | ||
116 | + /** | ||
117 | + * Tests scheduling for a particular date or time which may be in the past. | ||
118 | + * | ||
119 | + * @throws Exception throws an exception if the test fails | ||
120 | + */ | ||
121 | + @Test | ||
122 | + public void testScheduleByDate() throws Exception { | ||
123 | + /* Tests basic scheduling for future times. */ | ||
124 | + timer.schedule(new TestTask(), new Date(10)); | ||
125 | + timer.advanceTimeMillis(5); | ||
126 | + assertFalse(taskList.get(0).hasRun()); | ||
127 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
128 | + assertTrue(taskList.get(0).hasRun()); | ||
129 | + | ||
130 | + /* Test scheduling with past times numbers */ | ||
131 | + timer.schedule(new TestTask(), new Date(0)); | ||
132 | + delay(REAL_TIME_DELAY); | ||
133 | + assertTrue(taskList.get(1).hasRun()); | ||
134 | + | ||
135 | + /* Tests cancellation on non-periodic events */ | ||
136 | + TestTask task = new TestTask(); | ||
137 | + timer.schedule(task, new Date(timer.currentTimeInMillis() + 10)); | ||
138 | + task.cancel(); | ||
139 | + timer.advanceTimeMillis(12, REAL_TIME_DELAY); | ||
140 | + assertFalse(task.hasRun()); | ||
141 | + | ||
142 | + } | ||
143 | + | ||
144 | + /** | ||
145 | + * Test scheduling beginning after a delay and recurring periodically. | ||
146 | + * | ||
147 | + * @throws Exception throws an exception if the test fails | ||
148 | + */ | ||
149 | + @Test | ||
150 | + public void testScheduleByDelayPeriodic() throws Exception { | ||
151 | + /* Test straightforward periodic execution */ | ||
152 | + timer.schedule(new TestTask(), 0, 10); | ||
153 | + delay(REAL_TIME_DELAY); | ||
154 | + assertEquals("Task should have run once when added.", 1, taskList.get(0).timesRun()); | ||
155 | + | ||
156 | + /* Tests whether things that are not added to the queue are scheduled for future executions (ones which execute | ||
157 | + immediately on add). */ | ||
158 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
159 | + assertEquals("Task should have run once when added.", 2, taskList.get(0).timesRun()); | ||
160 | + | ||
161 | + /* Tests whether cancellation works on periodic events. */ | ||
162 | + taskList.get(0).cancel(); | ||
163 | + | ||
164 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
165 | + assertEquals("The task should not have run another time.", 2, taskList.get(0).timesRun()); | ||
166 | + | ||
167 | + TestTask task = new TestTask(); | ||
168 | + timer.schedule(task, 0, 10); | ||
169 | + timer.advanceTimeMillis(100, REAL_TIME_DELAY); | ||
170 | + assertEquals("Should have run immeditaley and subsequently once during the larger skip", task.timesRun(), 2); | ||
171 | + | ||
172 | + } | ||
173 | + | ||
174 | + /** | ||
175 | + * Test scheduling beginning at a specified date and recurring periodically. | ||
176 | + * | ||
177 | + * @throws Exception throws an exception if the test fails | ||
178 | + */ | ||
179 | + @Test | ||
180 | + public void testScheduleByDatePeriodic() throws Exception { | ||
181 | + /* Test straightforward periodic execution */ | ||
182 | + timer.schedule(new TestTask(), new Date(timer.currentTimeInMillis()), 10); | ||
183 | + delay(REAL_TIME_DELAY); | ||
184 | + assertEquals("Task should have run once when added.", 1, taskList.get(0).timesRun()); | ||
185 | + | ||
186 | + /* Tests whether things that are not added to the queue are scheduled for future executions (ones which execute | ||
187 | + immediately on add). */ | ||
188 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
189 | + assertEquals("Task should have run once when added.", 2, taskList.get(0).timesRun()); | ||
190 | + | ||
191 | + /* Tests whether cancellation works on periodic events. */ | ||
192 | + taskList.get(0).cancel(); | ||
193 | + | ||
194 | + timer.advanceTimeMillis(10, REAL_TIME_DELAY); | ||
195 | + assertEquals("The task should not have run another time.", 2, taskList.get(0).timesRun()); | ||
196 | + | ||
197 | + TestTask task = new TestTask(); | ||
198 | + timer.schedule(task, new Date(timer.currentTimeInMillis()), 10); | ||
199 | + timer.advanceTimeMillis(100, REAL_TIME_DELAY); | ||
200 | + assertEquals("Should have run immediately and subsequently once during the larger skip", task.timesRun(), 2); | ||
201 | + } | ||
202 | + | ||
203 | + /* Schedule at fixed rate runs exactly like the two scheduling methods just tested so tests are not included */ | ||
204 | + | ||
205 | + /** | ||
206 | + * Timer task with added functions to make it better for testing. | ||
207 | + */ | ||
208 | + private class TestTask extends TimerTask { | ||
209 | + | ||
210 | + /* Remains true once the task has been run at least once */ | ||
211 | + private boolean hasRun; | ||
212 | + | ||
213 | + /* Unique id per event. */ | ||
214 | + private int id; | ||
215 | + | ||
216 | + /* Specifies the number of times an event has run */ | ||
217 | + private int timesRun; | ||
218 | + | ||
219 | + /** | ||
220 | + * Constructor initializes id, timesRun, and id fields. | ||
221 | + */ | ||
222 | + public TestTask() { | ||
223 | + id = idGenerator.getAndIncrement(); | ||
224 | + timesRun = 0; | ||
225 | + hasRun = false; | ||
226 | + taskList.add(this); | ||
227 | + } | ||
228 | + | ||
229 | + @Override | ||
230 | + public void run() { | ||
231 | + this.hasRun = true; | ||
232 | + tasksRunCount.incrementAndGet(); | ||
233 | + timesRun++; | ||
234 | + } | ||
235 | + | ||
236 | + /** | ||
237 | + * Returns whether this event has run. | ||
238 | + * | ||
239 | + * @return true if the event has run, false otherwise. | ||
240 | + */ | ||
241 | + public boolean hasRun() { | ||
242 | + return hasRun; | ||
243 | + } | ||
244 | + | ||
245 | + /** | ||
246 | + * Returns the number of times this task has run. | ||
247 | + * | ||
248 | + * @return an int representing the number of times this task has been run | ||
249 | + */ | ||
250 | + public int timesRun() { | ||
251 | + return timesRun; | ||
252 | + } | ||
253 | + | ||
254 | + /** | ||
255 | + * Returns the unique identifier of this task. | ||
256 | + * | ||
257 | + * @return a unique integer identifier | ||
258 | + */ | ||
259 | + public int getId() { | ||
260 | + return id; | ||
261 | + } | ||
262 | + } | ||
263 | +} | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or login to post a comment