You are on page 1of 27

TAFJ-U ni tT e s t F r a m e w o r k

R16
TAFJ-U ni tT e s t F r a m e w o r k

Amendment History:

Revisio
Date Amended Name Description
n
1 Sep 11 2015 T. Aubert Initial version
2 Nov 5 2015 T. Aubert New functionalities added
3 Dec 15 2015 T. Aubert Removed the recording facilities

4 15th March 2016 M. Kumar R16 AMR review

Page 2
TAFJ-U ni tT e s t F r a m e w o r k

Ta b l e o f C o n t e n t s
Introduction......................................................................................................................... 6

tut Overview........................................................................................................................ 6

The UTF “Component”........................................................................................................7

Global UTF Methods........................................................................................................... 9

UTF.setTarget(<subroutine>)..........................................................................................9

UTF.setDescription(<a_description>).............................................................................9

UTF.addParam(<variable_or_value>)............................................................................9

UTF.setRecord(<table>, <id>, <record>)......................................................................10

UTF.runTest()............................................................................................................... 10

Stubs methods.................................................................................................................. 10

reference = UTF.addStub(<subroutine>)......................................................................10

UTF.setRepeatable(<a_stub_ref>)...............................................................................10

UTF.addStubParam(<a_stub_ref>, <in_value>, <out_value>)......................................11

UTF.addStubCommonChange(<a_stub_ref>,<common>,<out_value>).......................11

UTF.addStubCacheChange(<a_stub_ref>, <bucket_name>, <key>, <out_value>)......12

UTF.addStubWriteDone(<a_stub_ref>,<table>,<id>, <out_record>)............................12

UTF.addStubPropertyChange(<a_stub_ref>,<component>,<property>, <value>).......13

UTF.setStubReturnValue(<a_stub_ref>, <value>)........................................................13

UTF.getNbInvocation(<a_stub_ref>)............................................................................13

Assertions methods.......................................................................................................... 14

UTF.addParam("7").......................................................................................................... 14

UTF.assertEquals(<value1>, <value2>).......................................................................14

UTF.assertContains(<value1>, <value2>)....................................................................14

UTF.assertTrue(<value>).............................................................................................14

Page 3
TAFJ-U ni tT e s t F r a m e w o r k

UTF.assertFalse(<value>)............................................................................................15

UTF.assertGreaterThan(<value1>, <value2>)..............................................................15

UTF.assertLessThan(<value1>, <value2>)..................................................................15

UTF.assertStartsWith(<value1>, <value2>)..................................................................15

UTF.assertEndsWith(<value1>, <value2>)...................................................................15

UTF.assertMatches(<value1>, <mask>).......................................................................15

UTF.getRecord(<table>, <id>)......................................................................................15

Modifiers........................................................................................................................... 16

UTF.any()..................................................................................................................... 17

UTF.matches(<mask>).................................................................................................17

UTF.same().................................................................................................................. 17

Creating a new UnitTest....................................................................................................18

From scratch................................................................................................................18

By Recording............................................................................................................... 19

Recording at Component level.....................................................................................20

Editor Helpers................................................................................................................... 22

Setting the target..........................................................................................................22

Adding a stub............................................................................................................... 23

JBC Subroutine selection dialog.......................................................................................24

Converting old tuts............................................................................................................ 25

Compiling, and distributing tests.......................................................................................25

Running tests....................................................................................................................26

In console mode...........................................................................................................26

Running multiple tests at once.....................................................................................27

In Eclipse..................................................................................................................... 28

Reporting.......................................................................................................................... 29

Component and UnitTests................................................................................................29

Page 4
TAFJ-U ni tT e s t F r a m e w o r k

Scoping exception........................................................................................................29

Stubbing a method.......................................................................................................29

Page 5
TAFJ-U ni tT e s t F r a m e w o r k

Intro d u c ti o n
The Unit Test Framework (UTF) helps developers and testers to validate that the code is
doing what it is intended to do. The version 1.0 was storing binary informations, and was
requiring a specific editor to modify a tests. Despite that the “binary” way of storing in
formations was a good approach for huge sets of informations, the downsides were too big,
and a full re-engineering has been done to get to the Version 2.
The Version 2 still have the recording facility, and this is much easier to maintain the test as
this ends up being “just” jBC code. However, unlike EasyQA, there is only one file per test
and the full Eclipse launching facilities are still available. This document aims to describe in
details how this all work. You will notice that there is no “deployment” section as everything is
embedded in the existing runtime / compiler / eclipse plugins.

t u t Ov e rvi e w
A .tut file is a jBC file. However, the extension .tut is necessary to help the different tools to
discover the tests.
A tut is not a SUBROUTINE, neither a PROGRAM nor a FUNCTION. This is a
“TESTCASE”.
So every tuts are starting with TESTCASE <name of test> as shown here :

We could wonder why this could not be a simple SUBROUTINE or PROGRAM, and later in
the document, everything will make sense. For now, we simply need to know that a UnitTest
is declared in the code with TESTCASE.
Then, this is just jBC, anything jBC can do can be done in a tut. However, this would not be
really useful to stop at that point, and this is here that the TESTCASE declaration starts to
make sense. In fact, by using TESTCASE, this will automatically add a low-level reference in
the code. This reference is “UTF”. IN the next chapter, we will describe all the methods the
UTF reference (object) is offering. We will call it a “component” to make the reading more jBC
oriented, but in fact this is a real Object.

T h e U T F “C o m p o n e n t ”

So we know now that a Temenos Unit Test is a file with the “tut” extension that this is written
in jBC and that the declaration is “TESTCASE” because we need the “UTF Component”
which will help us doing a test. So let's look in details at this Component.

Page 6
TAFJ-U ni tT e s t F r a m e w o r k

First, this is a low-level component, sitting into the runtime, so don't try to find something like
UTF.component on your disk, it doesn't exists. Then, this component is automatically “here”.
No need to do a $USING or whatsoever, it sits behind your code. You can consider it as a
language extension. Let's explore all these extensions. The full list :
Global methods :

UTF.runTest()

UTF.setTarget(<subroutine>)

UTF.setDescription(<a_description>)

UTF.addParam(<variable_or_value>)

UTF.setRecord(<table>, <id>, <record>)

UTF.runTest()

Stubs methods :

UTF.addStub(<subroutine>)

UTF.setRepeatable(<a_stub_ref>)

UTF.addStubParam(<a_stub_ref>, <in_value>, <out_value>)

UTF.addStubCommonChange(<a_stub_ref>, <common>, <out_value>)

UTF.addStubCacheChange(<a_stub_ref>, <bucket_name>, <key>,


<out_value>)

UTF.addStubWriteDone(<a_stub_ref>, <table>, <id>, <out_record>)

UTF.addStubPropertyChange(<a_stub_ref>, <component>, <property>,


<value>)

UTF.setStubReturnValue(<a_stub_ref>, <value>)

Assertions methods :

UTF.assertEquals(<value1>, <value2>)

UTF.assertContains(<value1>, <value2>)

UTF.assertTrue(<value>)

UTF.assertFalse(<value>)

Page 7
TAFJ-U ni tT e s t F r a m e w o r k

UTF.assertGreaterThan(<value1>, <value2>)

UTF.assertLessThan(<value1>, <value2>)

UTF.assertStartsWith(<value1>, <value2>)

UTF.assertEndsWith(<value1>, <value2>)

UTF.assertMatches(<value1>, <mask>)

UTF.getRecord(<table>, <id>)

Modifiers methods :

UTF.any()

UTF.matches(<mask>)

UTF.same()

Page 8
TAFJ-U ni tT e s t F r a m e w o r k

Gl o b a l U T F M e t h o d s
U TF. s e tT ar g e t ( < s u b r o u t i n e > )
Usually the very first method (but not mandatory), this is defining what subroutine we want to
test. This subroutine will be called with the UTF.runTest() method described later.

You can pass a variable as well as a constant value.

XYZ = "TEST.SIMPLE"

UTF.setTarget(XYZ)

or

UTF.setTarget("TEST.SIMPLE")

Note that this will be always the same for all parameter, so we won't repeat it.

U TF. s e t D e s c r i p t i o n ( < a_ d e s c r i p t i o n > )


This is setting a description for the test (for reporting purposes)

U TF. a d d P a r a m ( < v a r i a b l e_ o r_v a l u e > )


This one needs to be called as many times as you have parameters in your target. Here, you
should (but not mandatory) put a variable, because this is what you will reference in the
assertions. (see later)

Here are two examples :

P1 = "7"

UTF.addParam(P1)

DIM P2(10)

P2(2) = "6"

UTF.addParam(P2)

Page 9
TAFJ-U ni tT e s t F r a m e w o r k

U TF. s e t R e c o r d ( < t a b l e > , < i d > , < r e c o r d > )


Since running the test is not touching at the database, if your test is dependent of a pre-
existing record, you must add this record with this method. This is a little bit as if you where
writing a record in a database, but this is kept in memory.

UTF.setRecord("F.MY.TEST", "MY.KEY.1", 'Field1' : @FM : "Field2")

U TF.r u nT e s t ( )
This method simply launch the test (call the routine defined by the setTarget(...). This method
must be called after the stubs definition (see next section)

If the routine defined by the setTarget is a FUNCTION, then UTF.runTest() will return the
value of the function.

Eg :

ret.value = UTF.runTest()

Stubs m ethods

r e f e r e n c e = U TF. a d d S t u b ( < s u b r o u t i n e > )


This is the declaring method for a stub (the replacement of a routine). Imagine your target is
calling the subroutine DO.SOMETHING, then you will declare it like this :

STUB = UTF.addStub("DO.SOMETHING")

Note that the return value is a function. You cannot do a lot with it appart passing it back to
the next stub related functions.

U TF. s e t R e p e a t a b l e ( < a_ s t u b_r e f > )


Normally, when the invocation of a sub is done, this one will not be re-used. As an example, if
the target is calling 2 times DO.SOMETHING, then you should have 2 stubs (you should put
2 UTF.addStub(“DO.SOMETHING”). However, by specifying that the stub is repeatable, this
save you to declare it twice. A typical routine in T24 which can be “repeatable” is “CDT”.

The section describing how the stubs are found (later in this document) will certainly bring
some lights.

Page 10
TAFJ-U ni tT e s t F r a m e w o r k

U TF. a d d S t u b P a r a m ( < a_ s t u b_r e f > , < i n_v a l u e > , < o u t_v a l u e > )
This method has to be invoked as many times as the number of parameters in your stub. In
DO.SOMETHING, we have 3 parameters, so we need to invoke 3 times
UTF.addStubParam(...). The first parameter is the stub references (obtained when doing
UTF.addStub), the second one is the in-parameter, the third one is the value of this
parameter after the call. We could argue about the in-parameter need. This one is used to
detect which stub needs to be called. If we take the same CDT function, we will have

STUB = UTF.addStub("CDT")

UTF.addStubParam(STUB, '', '')

UTF.addStubParam(STUB, "20130623", "20130624")

UTF.addStubParam(STUB, "+1C", "+1C")

STUB = UTF.addStub("CDT")

UTF.addStubParam(STUB, '', '')

UTF.addStubParam(STUB, "20130523", "20130524")

UTF.addStubParam(STUB, "+1C", "+1C")

When the target calls CDT('', 20130623, “+1C”), then the 2nd parameter changes to
20130624. The tests needs to know which one to take. This explain the in-parameter.

Note that STUB is just a variable name, you can put anything you want.

MY.FIRST.STUB = UTF.addStub("CDT")

UTF.addStubParam(MY.FIRST.STUB, '', '')

. . .

U TF. a d d S t u b C o m m o n C h a n g e ( < a_ s t u b_r e f > , < c o m m o n > , < o u t_v a l u e


>)
Here, this is how we say that the subroutine is supposed to change the value of a common
variable. Remember, since the routine is never really called, we need to tell everything. Of
course, only common variable having an impact on the target routine are necessary.

Page 11
TAFJ-U ni tT e s t F r a m e w o r k

UTF.addStubCommonChange(STUB, COMI, "A NEW VALUE FOR COMI")

If your common is an DIMmed array (eg R.NEW), you will pass an array as new value :

DIM stubCommon(500)

stubCommon(10) = "A New Value for R.NEW"

UTF.addStubCommonChange(STUB, R.NEW, stubCommon)

U TF. a d d S t u b C a c h e C h a n g e ( < a_ s t u b_r e f > , < b u c k e t_ n a m e > , < k ey >,


< o u t_v a l u e > )
Same approach as for the common, appart from the fact that you are not passing the
common itself, but the Bucket name and the key.

Doing something like that :

UTF.addStubCacheChange(STUB, "USED.BUCKET", "KEY1", "A New Value")

Is the equivalent as the real routine doing

CachePut("USED.BUCKET", "KEY1", "A New Value")

U TF. a d d S t u b Wri t e D o n e ( < a_ s t u b_r e f > , < t a b l e > , < i d > , < o u t_r e c o r d > )
Exactly the same as the previous one, but for database.

So doing

UTF.addStubWriteDone(STUB, "F.MY.TEST", "MY.KEY.1", "A new Record")

is the same as if the routine were doing a F.WRITE. Of course, the database is not touched,
but this is “as if ...”

Page 12
TAFJ-U ni tT e s t F r a m e w o r k

So after the stub invocation, the target routine can do a

CALL F.READ("F.MY.TEST", "MY.KEY.1", REC, F.MY.TEST, ERR)

In REC, we would find : "A new Record"

U TF. a d d S t u b P r o p e r t yC h a n g e ( < a_ s t u b_r e f > , < c o m p o n e n t > , < p r o p


erty >, < v al u e > )
When using components, COMMON variables are not accessible directly. Instead, these are
getters / setters on a property defined n the component. Use the addStubPropertyChange
method, giving the component name and it's property for changing it when the stub is
invoked.

As an example, if a routine was doing

MOD.MyComponent.setMyProperty('a new value')

you will tell the stub the following :

UTF.addStubPropertyChange(STUB, 'MOD.MyComponent', 'MyProperty', 'a new value')

U TF. s e t S t u b R e t u r nVal u e ( < a_ s t u b_r e f > , < v a l u e > )


If a stubb is a function, you will specify the return value of the function with this method

U TF. g e t N b I n v o c a t i o n ( < a_ s t u b_r e f > )

Return the number of time a stub got invoked during the test
example (check that our stub got invoked 2 times) :

NB.STUB = UTF.getNbInvocation(STUB)

UTF.assertEquals(2, NB.STUB)

Page 13
TAFJ-U ni tT e s t F r a m e w o r k

As s ertio n s m e t h o d s
These methods are obvious and talking by themselves. If one of these method is evaluated
as “false”, the test will be in “failure”. Please refer to the “reporting” and “eclipse” chapters for
more details.

One small note on the parameters.

At the beginning of the document, we mentioned that the parameters should be put as
variables and not constant. Imagine that you wrote

UTF.addParam("7")

Instead of

P1 = "7"

UTF.addParam(P1)

This would make no sense to make an assertion on “7” : UTF.assertEquals("7", "7")

But verifying that the first parameter is not changing :

UTF.assertEquals(P1, "7")

Makes suddenly more sense.

U TF. a s s e r t E q u a l s ( < v a l u e 1 > , < v a l u e 2 > )


TRUE if <value1> is equals to <value2>, FALSE otherwise

U TF. a s s e r tC o n t a i n s ( < v a l u e 1 > , < v a l u e 2 > )


TRUE if <value1> contains <value2>, FALSE otherwise

U TF. a s s e r tTr u e ( < v a l u e > )


TRUE if <value1> is evaluated (jBC) to TRUE (same as IF <value1> ...)

U TF. a s s e r tF a l s e ( < v a l u e > )


TRUE if <value1> is evaluated (jBC) to FALSE (same as IF NOT(<value1>) ...)

Page 14
TAFJ-U ni tT e s t F r a m e w o r k

U TF. a s s e r tGr e a t e rT h a n ( < v a l u e 1 > , < v a l u e 2 > )


TRUE if <value1> GT <value2>, FALSE otherwise

U TF. a s s e r tL e s s T h a n ( < v a l u e 1 > , < v a l u e 2 > )


TRUE if <value1> LT <value2>, FALSE otherwise

U TF. a s s e r t S t a r t s W i t h ( < v a l u e 1 > , < v a l u e 2 > )


TRUE if <value1> Starts With <value2>, FALSE otherwise

U TF. a s s e r t E n d s W i t h ( < v a l u e 1 > , < v a l u e 2 > )


TRUE if <value1> Ends With <value2>, FALSE otherwise

U TF. a s s e r t M a t c h e s ( < v a l u e 1 > , < m a s k > )


TRUE if <value1> MATCHES <value2>, FALSE otherwise

This is the MATCH equivalent of jBC

eg

UTF.assertMatches(P1, "3N4X")

P1 = "123ABCD" : TRUE

P1 = "123ABC" : FALSE

U TF. g e t R e c o r d ( < t a b l e > , < i d > )


This is fetching a record from the (in-memory database) useful for making assertions on the
record.

REC = UTF.getRecord("F.MY.TEST", "MY.KEY.2")

UTF.assertEquals(REC, 'A new Record from stub' : @FM : "Modified")

Page 15
TAFJ-U ni tT e s t F r a m e w o r k

M o difi er s
This section should in fact be linked to the Stubs, but for clarity of the documents let's keep it
at the end. Modifiers are informations you can pass as arguments to the stub definitions.

When a test is running, and the target is making a CALL, the UTF is looking for the best
matching stub. The first absolute test is the stub name (the SUBROUTINE name must match
EXACTELY the call -obvious-). Then, we will check the parameters.

If all the parameters are “normally” defined, then they must match exactly as well, however,
we can put modifiers so the stub could not fully match.

Example :

UTF.addStubParam(STUB, UTF.any(), "Anything")

Means that whatever the in parameter is, this stub will be eligible for the CALL and the
parameter will be changed to "Anything"

UTF.addStubParam(STUB, UTF.matches("2N"), "less than hundred")

Means that if the in parameter Matches "2N", this stub will be eligible for the CALL and the
parameter will be changed to "less than hundred"

So what if we have multiple stubs being eligible ?

The rules is the following.

The stubs are sorted by <number of strict value>, <number of match value> and <number of
“any” value>. The top most stub will be taken.

In the precedent example, UTF.matches("2N") is better than UTF.any(). However, if we


had UTF.addStubParam(STUB, "12", "twelve"), and the parameter was "12", this is the
one which would have been taken.

Page 16
TAFJ-U ni tT e s t F r a m e w o r k

U TF. a n y ( )
Do not consider the parameter to elect the stub as a good stub for a given CALL.

U TF. m a t c h e s ( < m a s k > )


Elect the stub as a good stub for a given CALL if the parameter is matching (jBC MATCH) the
given mask.

U TF. s a m e ( )
UTF.same() is not a modifier as such. It is defined for the out-parameter. This is basically
saying that the out parameter ends up having the same value as the in. As an example, this
parameter :

UTF.addStubParam(STUB, UTF.matches("2N"), UTF.same())

Would not change the parameter if this is a 2 digit number, and of course, if there is no better
match (eg UTF.addStubParam(STUB, "12", "twelve"))

Page 17
TAFJ-U ni tT e s t F r a m e w o r k

Cr e a t i n g a n e w U n i tT e s t

Fr o m s c r a t c h
Creating a test is not any different than creating a new SUBROUTINE : In Eclipse, Right-click
on the directory where you want to create your test, select

New - > T24 routine / component / testcase

Select “Create UnitTest”, give a name to your file …

Page 18
TAFJ-U ni tT e s t F r a m e w o r k

And automatically a new TestCase, with a template will be created as follows :

Then this is just a matter to edit / save / run.

Page 19
TAFJ-U ni tT e s t F r a m e w o r k

Edit or H e l p er s
When editing a TESTCASE, you have some options in the contextual menu of your editor.
Righ-click on the editor (where you want to add something). You will have the “Unit Test
Framework ….” menu available with 2 options : “Setting the target...” and “Adding a stub...”

S e tti n g t h e tar g e t
This option will open the JBC Subroutine selection dialog (see specific chapter).

Select a routine or component method, and the code for setting the target with all parameters
will be added. You only will need to specify the values of the parameters.

Example by choosing AA.Fees.CalculateTierAmount(...) :

UTF.setTarget("AA.Fees.CalculateTierAmount")

TierGroup = ''

UTF.addParam(TierGroup)

TierType = ''

UTF.addParam(TierType)

. . .

ChargeCalcDetails = ''

UTF.addParam(ChargeCalcDetails)

RetError = ''

UTF.addParam(RetError)

You just need to specify values to your variables.

Page 20
TAFJ-U ni tT e s t F r a m e w o r k

Ad d i n g a s t u b
Adding a stub is done the same way as setting the target. The code will be generated like
this ( for AC.API.EbUpdateAcctEntFwd )

STUB = UTF.addStub("AC.API.EbUpdateAcctEntFwd")

UTF.addStubParam(STUB, UTF.any(), UTF.same()) ;* AccountId

UTF.addStubParam(STUB, UTF.any(), UTF.same()) ;* Action

UTF.addStubParam(STUB, UTF.any(), UTF.same()) ;* EntryId

UTF.addStubParam(STUB, UTF.any(), UTF.same()) ;* ErrorCode

The default values are UTF.any() for the IN param and UTF.same() for the OUT param. You
will need to change these values according to your test case.

Page 21
TAFJ-U ni tT e s t F r a m e w o r k

JBC S u b r o u t i n e s e l e c t i o n d i a l o g

The precedent chapter is mentionning this dialog box, and here is a description of how it
works.

IMPORTANT : The contents of this dialog box IS NOT THE SOURCE files, but the
compiled one. This means you don't need to have all sources to be able to select a
routine. Just have the jars in your precompile directory.

Page 22
TAFJ-U ni tT e s t F r a m e w o r k

Co nv erti n g o l d t ut s
There is a tool to convert tut version 1.0 to the new 2.0 version.
In the TAFJ/bin directory, you will find the tConvertTest (.bat file).
The syntax is :

tConvertTest <file_to_convert> [<new_file>]

If the new file is omitted, the <file_to_convert> will be overridden by the new one.

Co m pili n g, a n d di s trib uti n g t e s t s

A test acts as a jBC PROGRAM or SUBROUTINE. There compile the same way, a Class is
generated, this class can be put in a jar.

Page 23
TAFJ-U ni tT e s t F r a m e w o r k

Runnin g tests

In c o n s ol e m o d e

Running a test is not very different as running a program. There is only one flag to specify : “-
test”. This flag is necessary as the class generated is prefixed with “testcase.” to not conflict
with the target subroutine.

To resume :

MY.SUBROUTINE.b generate a class called MY_SUBROUTINE_cl.class

MY.SUBROUTINE.tut generate a class called testcase_MY_SUBROUTINE_cl.class

example : tRun MY.SUBROUTINE will try to run the class MY_SUBROUTINE_cl.class which
is wrong. You have to run :

tRun -test MY.SUBROUTINE in order to run testcase_MY_SUBROUTINE_cl.class.

So this is

tRun -test myTest

Where “myTest” is the name you gave here :

TESTCASE myTest

You can also run your test by specifying the full file name.

Example :

tRun -test /path/to/my/top/dev/directory/unitTests/myTest.tut

Page 24
TAFJ-U ni tT e s t F r a m e w o r k

Run ni n g m ultiple t e st s at o n c e

You can also specify a directory. In that case, all tests in this directory will be run

Example :

tRun -test /path/to/my/top/dev/directory/unittests

And last but not least, you can use the -recurs flag if you want to find tests resursively in a
directory and run them :

Example :

tRun -test -recurs /path/to/my/top/dev/directory

R et ur n valu e

In console mode, the exit value of tRun will be 1 if one or more test are failing. A success
value is 0.

Note that a failure can be something else than a wrong assertion. It can be a call to a non
existing routine, a test without assertion, or any kind of technical failure.

Page 25
TAFJ-U ni tT e s t F r a m e w o r k

In Eclip s e
Running a test in eclipse consist of selecting

1) One *.tut file

2) Multiple *.tut files

3) One or multiple directories (containing “at least” one tut file)


Right-Click - > Run (or Debug) As … - > Unit Tests

Automatically, a new window will open showing the tests and their results :

Page 26
TAFJ-U ni tT e s t F r a m e w o r k

R e p ortin g
You can generate junit types of reports by specifying it in the properties file with the following
keys :
temn.tafj.utf.generate.results = true

By default, the reports (xml files) are generated in

<tafj_home>/data/<projectName>/testResults/

You can change this default location with the key

temn.tafj.utf.results.directory = …

C o m p o n e n t a n d U n i tT e s t s

Sc opin g exc eption


You can use $USING / $PACKAGE as any other jBC file. A small distinguishing : The scoping
do not apply when writing a TESTCASE. This means you can access private methods of any
component.

Stubbin g a m et hod
A method can be stubbed by giving it fully qualified name

UTF.addStub(“MOD.Component.myMethod”)

You do not need to explicitly add a $USING for this stub case.

Page 27

You might also like