1
+ @file:Suppress(" unused" )
2
+
1
3
package bps.console
2
4
3
5
import bps.console.app.MenuApplication
@@ -18,8 +20,7 @@ import kotlin.concurrent.thread
18
20
/* *
19
21
* This fixture allows the application and the test validations to interact with each other in a thread-safe way.
20
22
* The application is run in its own thread but it will pause between tests to allow the test code to validate
21
- * the outputs (and allowing the JDBC connection to remain open between tests). Using the [validateInteraction]
22
- * function will work for most use cases.
23
+ * the outputs.
23
24
*
24
25
* Usage:
25
26
*
@@ -71,6 +72,7 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
71
72
fun validateInteraction (expectedOutputs : List <String >, toInput : List <String >) {
72
73
require(toInput.isNotEmpty())
73
74
outputs.clear()
75
+ inputs.shouldBeEmpty()
74
76
inputs.addAll(toInput)
75
77
waitForApplicationProcessing()
76
78
inputs.shouldBeEmpty()
@@ -145,7 +147,9 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
145
147
146
148
companion object {
147
149
148
- enum class Owner {
150
+ class ApplicationExitException (msg : String ) : Exception(msg)
151
+
152
+ enum class AppStatus {
149
153
/* *
150
154
* Indicates that the test is starting up and either thread might try to update data first.
151
155
*/
@@ -154,12 +158,12 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
154
158
/* *
155
159
* Indicates that the application thread owns the permit on [takeTurns]
156
160
*/
157
- APP ,
161
+ RUNNING ,
158
162
159
163
/* *
160
164
* Indicates that the application is waiting for input from the test.
161
165
*/
162
- TEST ,
166
+ PAUSED ,
163
167
164
168
/* *
165
169
* Indicates that the application thread has exited.
@@ -178,29 +182,52 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
178
182
* Should only be set by the application thread while it has a permit on [takeTurns]
179
183
*
180
184
* INVARIANTS:
181
- * 1. This will only change values when the application thread owns the permit on [takeTurns]
182
- * 2. If the value is APP, then the application thread definitely owns a permit
183
- * 3. This is set to TEST when the application is waiting for input
185
+ * 1. This will only change values when the application thread owns the permit on [takeTurns].
186
+ * 2. If the value is [AppStatus.RUNNING], then the application thread definitely owns a permit.
187
+ * 3. If the application is paused, then this will be [AppStatus.PAUSED].
188
+ * 4. If the application is terminated, then this will be [AppStatus.APP_DONE].
189
+ * 5. If the application has not yet tried to read input or produce output, then this will be [AppStatus.START_UP].
190
+ */
191
+ @Volatile
192
+ private var appStatus: AppStatus = AppStatus .START_UP
193
+
194
+ /* *
195
+ * INVARIANTS:
196
+ * 1. If this is `true`, then the test has prepared some input and the application has not acknowledged it.
197
+ * 2. If this is `false`, then the TEST is free to take action. The test should wait until this is `false` before taking back control unless [appStatus] is [AppStatus.APP_DONE]
198
+ * 3. This value will only be changed by a thread that has a permit on [takeTurns]
199
+ *
200
+ * The application will set this to `false` when it wakes up from waiting for a permit on [takeTurns].
201
+ *
202
+ * The application will only change this value when [appStatus] is [AppStatus.RUNNING].
203
+ *
204
+ * The test will only change this value when [appStatus] is not [AppStatus.RUNNING].
184
205
*/
185
206
@Volatile
186
- private var owner : Owner = Owner . START_UP
207
+ private var applicationHasUnackedTurnover : Boolean = false
187
208
188
209
/* *
189
210
* Ensure the application and test aren't messing each other up.
190
211
*
191
212
* Should never have more than one permit.
192
213
*
193
- * When [owner ] is [Owner .START_UP], the application cannot output until the test is ready. That
214
+ * When [appStatus ] is [AppStatus .START_UP], the application cannot output until the test is ready. That
194
215
* readiness is indicated by the test creating a permit with a call to [Semaphore.release].
195
216
*/
196
217
// NOTE the test is expected to release (or create) a permit when it has inputs ready for the application
197
218
// NOTE application should be careful not to call release unless it has a permit otherwise another permit will be created
198
219
private val takeTurns = object : Semaphore (0 , true ) {
199
- override fun acquire () {
220
+
221
+ // fun applicationTryAquire(timeout: Long, unit: TimeUnit?): Boolean {
222
+ // // NOTE this is a not-completely-effective way to try to catch cases when there are plural permits
223
+ // return tryAcquire(timeout, unit)
224
+ // }
225
+
226
+ override fun tryAcquire (timeout : Long , unit : TimeUnit ? ): Boolean {
200
227
// NOTE this is a not-completely-effective way to try to catch cases when there are plural permits
201
228
if (availablePermits() > 1 )
202
229
throw IllegalStateException (" takeTurns must never have more than one permit" )
203
- super .acquire( )
230
+ return super .tryAcquire(timeout, unit )
204
231
}
205
232
206
233
override fun release () {
@@ -220,53 +247,67 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
220
247
override val outputs = CopyOnWriteArrayList <String >()
221
248
222
249
override val inputReader = InputReader {
223
- if (owner == = Owner .APP ) {
224
- if (inputs.isNotEmpty())
225
- inputs.removeFirst()
226
- else {
227
- // application expected input that wasn't there
228
- owner = Owner .APP_DONE
229
- takeTurns.release()
230
- throw IllegalStateException (" application was expecting more input than what was provided" )
250
+ when (appStatus) {
251
+ AppStatus .RUNNING -> {
252
+ if (inputs.isNotEmpty())
253
+ inputs.removeFirst()
254
+ else {
255
+ // application expected input that wasn't there
256
+ applicationHasUnackedTurnover = false
257
+ appStatus = AppStatus .APP_DONE
258
+ takeTurns.release()
259
+ throw ApplicationExitException (" application was expecting more input than what was provided" )
260
+ }
231
261
}
232
- } else if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
233
- owner = Owner .APP_DONE
234
- throw IllegalStateException (" Unable to acquire permit for application thread" )
235
- } else {
236
- // NOTE at this point, the application thread has a permit and owns the data.
237
- owner = Owner .APP
238
- inputs.removeFirst()
262
+ AppStatus .START_UP , AppStatus .PAUSED -> {
263
+ if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
264
+ appStatus = AppStatus .APP_DONE
265
+ throw ApplicationExitException (" Unable to acquire permit for application thread within $awaitMillis milliseconds" )
266
+ } else {
267
+ // NOTE at this point, the application thread has a permit and owns the data.
268
+ appStatus = AppStatus .RUNNING
269
+ inputs.removeFirst()
270
+ }
271
+ }
272
+ AppStatus .APP_DONE -> throw ApplicationExitException (" application was caught asking for input after reporting it had terminated" )
239
273
}
240
274
}
241
275
242
276
// NOTE the application can only output when it owns a permit on [takeTurns]
243
277
// NOTE when the inputs is empty, the application will release the permit, set the owner as TEST and wait for another permit
244
278
// NOTE only the application calls this
279
+ // TODO how about allow output when inputs is empty?
280
+ // 1. the application would start editing outputs right away (may not be a problem as long as test doesn't check those until application is paused.)
281
+ // 2. test would have to expect outputs in a different way
245
282
override val outPrinter = OutPrinter {
246
- // FIXME there is probably a slight race condition here. On startup, the app might get to this code
247
- // at the same moment the test is turning over control. Can we say that only the app thread
248
- // can change the value of owner?
249
- if (owner != = Owner . APP ) {
250
- if ( ! takeTurns.tryAcquire(awaitMillis, TimeUnit . MILLISECONDS )) {
251
- owner = Owner . APP_DONE
252
- throw IllegalStateException ( " Unable to acquire permit for application thread " )
253
- } else {
254
- // NOTE at this point, the application thread has a permit and owns the data.
255
- owner = Owner . APP
283
+ when (appStatus) {
284
+ AppStatus . RUNNING -> {}
285
+ AppStatus . START_UP , AppStatus . PAUSED , AppStatus . APP_DONE -> {
286
+ if (! takeTurns.tryAcquire(awaitMillis, TimeUnit . MILLISECONDS ) ) {
287
+ appStatus = AppStatus . APP_DONE
288
+ throw ApplicationExitException ( " Unable to acquire permit for application thread within $awaitMillis milliseconds " )
289
+ } else {
290
+ // NOTE at this point, the application thread has a permit and owns the data.
291
+ appStatus = AppStatus . RUNNING
292
+ }
256
293
}
257
294
}
258
- // NOTE at this point, the application thread has a permit and owns the data.
295
+ // NOTE at this point, appStatus is RUNNING and the application thread has a permit.
296
+ // NOTE pause if there are no inputs
259
297
while (inputs.isEmpty()) {
260
- owner = Owner .TEST
261
- takeTurns.release()
262
- // NOTE the semaphore is fair so the test thread should pick it up.
263
- // NOTE we want to allow this to be interrupted by the test thread
264
- if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
265
- owner = Owner .APP_DONE
266
- throw IllegalStateException (" Unable to acquire permit for application thread" )
267
- } else {
268
- owner = Owner .APP
269
- }
298
+ // NOTE can there be a race condition here? I think not because the app thread owns a permit
299
+ applicationHasUnackedTurnover = false
300
+ do {
301
+ appStatus = AppStatus .PAUSED
302
+ takeTurns.release()
303
+ // NOTE we want to allow this to be interrupted by the test thread
304
+ if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
305
+ appStatus = AppStatus .APP_DONE
306
+ throw ApplicationExitException (" Unable to acquire permit for application thread within $awaitMillis milliseconds" )
307
+ } else {
308
+ appStatus = AppStatus .RUNNING
309
+ }
310
+ } while (! applicationHasUnackedTurnover)
270
311
}
271
312
// NOTE at this point, the application thread has a permit and owns the data.
272
313
outputs.add(it)
@@ -278,31 +319,45 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
278
319
*/
279
320
// NOTE only the test thread calls this
280
321
override fun waitForApplicationProcessing () {
281
- when (owner) {
282
- Owner .TEST , Owner .START_UP -> {
283
- owner = Owner .APP
322
+ when (appStatus) {
323
+ AppStatus .PAUSED , AppStatus .START_UP -> {
324
+ applicationHasUnackedTurnover = true
325
+ // appStatus = AppStatus.RUNNING
284
326
takeTurns.release()
285
327
}
286
- Owner .APP_DONE -> fail(" application exited unexpectedly" )
287
- Owner .APP -> fail(" test and application running simultaneously" )
288
- }
289
- while (owner != = Owner .APP_DONE && outputs.isEmpty()) {
290
- println (" Test waiting for application thread" )
291
- if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
328
+ AppStatus .APP_DONE -> fail(" application exited unexpectedly" )
329
+ AppStatus .RUNNING -> {
292
330
stopApplication()
293
- fail(" Unable to acquire permit from application thread" )
331
+ fail(" test and application running simultaneously" )
332
+ }
333
+ }
334
+ if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
335
+ stopApplication()
336
+ fail(" Unable to acquire permit from application thread within $awaitMillis milliseconds" )
337
+ } else {
338
+ // NOTE this thread has a permit
339
+ while (appStatus != = AppStatus .APP_DONE && applicationHasUnackedTurnover) {
340
+ // NOTE must be that the application hasn't picked up the permit, yet. We need to keep giving it chances.
341
+ println (" Test waiting for application thread to take a permit" )
342
+ takeTurns.release()
343
+ if (! takeTurns.tryAcquire(awaitMillis, TimeUnit .MILLISECONDS )) {
344
+ stopApplication()
345
+ fail(" Unable to acquire permit from application thread within $awaitMillis milliseconds" )
346
+ }
294
347
}
295
348
}
296
349
}
297
350
298
351
override fun validateApplicationTerminated () {
299
352
applicationThread.join(awaitMillis)
300
- applicationThread.state shouldBe Thread .State .TERMINATED
301
- owner shouldBe Owner .APP_DONE
353
+ withClue(" application thread should have terminated within $awaitMillis milliseconds" ) {
354
+ applicationThread.state shouldBe Thread .State .TERMINATED
355
+ }
356
+ appStatus shouldBe AppStatus .APP_DONE
302
357
}
303
358
304
359
override fun validateApplicationPaused () {
305
- owner shouldNotBe Owner .APP_DONE
360
+ appStatus shouldNotBe AppStatus .APP_DONE
306
361
applicationThread.state shouldNotBe Thread .State .TERMINATED
307
362
}
308
363
@@ -318,10 +373,12 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
318
373
override fun close () {
319
374
application.close()
320
375
// NOTE immediately give control back to test
321
- val priorOwner = owner
322
- owner = Owner .APP_DONE
323
- if (priorOwner == = Owner . APP ) {
376
+ if (appStatus == AppStatus . RUNNING || applicationHasUnackedTurnover) {
377
+ appStatus = AppStatus .APP_DONE
378
+ applicationHasUnackedTurnover = false
324
379
takeTurns.release()
380
+ } else {
381
+ appStatus = AppStatus .APP_DONE
325
382
}
326
383
}
327
384
}
@@ -335,7 +392,10 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture {
335
392
// NOTE only test code calls this
336
393
override fun stopApplication () {
337
394
applicationThread.interrupt()
338
- applicationThread.join()
395
+ applicationThread.join(awaitMillis)
396
+ withClue(" application thread should have terminated within $awaitMillis milliseconds" ) {
397
+ applicationThread.state shouldBe Thread .State .TERMINATED
398
+ }
339
399
println (" Application stopped" )
340
400
}
341
401
0 commit comments