How to capture text from a PDF file

Capturing text from the Adobe Acrobat application

Important Note:
QuickTest Professional does not have special support for working with Acrobat or .PDF files. The content of this article is provided on an “as is” basis and is not part of QuickTest Professional. It is not guaranteed to work and is not supported by Mercury Customer Support. You are responsible for any and all modifications that may be required.

Please be aware that the steps needed to capture text may change with different versions of Adobe Acrobat Reader.

Acrobat Reader 6.0
QuickTest Professional will not be able to capture the text from the Acrobat window using its built in functionality, but it should still be possible to get the text.

1. Enable text selection in Acrobat Reader.
2. Select the text you wish to capture.
3. Copy the text to the system clipboard.
4. Use the Clipboard object to retrieve the text.
5. Once the text is in a variable, you can use VBScript string functions (e.g., InStr, Left, Right, Mid, Split) to parse through the string and get information out of it.

Example:
‘ This function enables Text Selection in Acrobat Reader 6.0
Public Function AcrobatEnableTextSelection()
‘ Press Alt+T, S, X to enable Text Selection in Acrobat Reader
Window(“regexpwndtitle:=Adobe Reader”,”regexpwndclass:=AdobeAcrobat”).Activate
Window(“regexpwndtitle:=Adobe Reader”,”regexpwndclass:=AdobeAcrobat”).Type micAltDwn + “t” + micAltUp
Window(“regexpwndtitle:=Adobe Reader”,”regexpwndclass:=AdobeAcrobat”).Type “s”
Window(“regexpwndtitle:=Adobe Reader”,”regexpwndclass:=AdobeAcrobat”).Type “x”
wait 0, 500
End Function

‘ This function copies the selected text to the system clipboard
Public Function AcrobatCopy(obj)
‘ Copy the selected text to the clipboard
obj.Type micCtrlDwn + “c” + micCtrlUp
End Function

‘ This function selects all the text in the PDF file
Public Function AcrobatSelectAll(obj)
obj.Click
obj.Type micCtrlDwn + “a” + micCtrlUp
End Function

‘ Selects the text in the specified coordinates. NOTE: The coordinates are relative to the object, not the screen.
Public Function AcrobatSelectPartial(obj, x1, y1, x2, y2)

‘ Calculate the screen coordinates for the text
ax = obj.GetROProperty(“abs_x”)
ay = obj.GetROProperty(“abs_y”)

sx = ax + x1
sy = ay + y1
ex = ax + x2
ey = ay + y2

‘ Select the text you wish to copy
Set DeviceReplay = CreateObject(“Mercury.DeviceReplay”)

DeviceReplay.MouseMove sx, sy
DeviceReplay.MouseDown sx, sy, 0
DeviceReplay.MouseMove ex, ey
DeviceReplay.MouseUp ex, ey, 0

Set DeviceReplay = Nothing
End Function

‘ Register the functions to the appropriate Test Object Classes.
RegisterUserFunc “WinObject”, “AcrobatSelectPartial”, “AcrobatSelectPartial”
RegisterUserFunc “WinObject”, “AcrobatSelectAll”, “AcrobatSelectAll”
RegisterUserFunc “WinObject”, “AcrobatCopy”, “AcrobatCopy”

‘ Instantiate the Clipboard object
Set cb = CreateObject(“Mercury.Clipboard”)

‘ Clear the Clipboard contents
cb.Clear

‘ Enable the Text Selection option
AcrobatEnableTextSelection()

‘ Select all the text in the pdf document and copy it to the clipboard.
Window(“Adobe Reader”).Window(“QTP_SWT_Support.pdf”).WinObject(“AVPageView”).AcrobatSelectAll
Window(“Adobe Reader”).Window(“QTP_SWT_Support.pdf”).WinObject(“AVPageView”).AcrobatCopy

‘ Get the text from the clipboard using the Clipboard object
pdfText = cb.GetText
msgbox pdfText

Set cb = Nothing

The above example selects the entire PDF file. You can also select text from specified coordinates.

Example:
‘ Select text in a specified location
‘ Instantiate the Clipboard object
Set cb = CreateObject(“Mercury.Clipboard”)

cb.Clear

‘ Put focus to the pdf document

Window(“Adobe Reader”).Window(“QTP_SWT_Support.pdf”).WinObject(“AVPageView”).Click 0,0

‘ Specify the coordinates
x1 = 170
y1 = 88
x2 = 543
y2 = 155

‘ Capture the text from within the specified coordinates. These coordinates are relative to the object, not the screen.
Window(“Adobe Reader”).Window(“QTP_SWT_Support.pdf”).WinObject(“AVPageView”).AcrobatSelectPartial x1, y1, x2, y2
Window(“Adobe Reader”).Window(“QTP_SWT_Support.pdf”).WinObject(“AVPageView”).AcrobatCopy

‘ Get the text from the clipboard using the Clipboard object
pdfText = cb.GetText
msgbox pdfText

Note:
If no text is selected by the AcrobatSelectPartial function, check the coordinates you used. If the mouse is not over an area that can be selected, QuickTest Professional will not be able to select the text. You can use Paint to help determine the coordinates; make sure you use calculate the coordinates using the specific object and not the entire Acrobat window.

Advertisements

COM/DCOM Recording

COM/DCOM Recording

Terminology and Definitions

Carray: Arrays that are similar to C arrays, but can dynamically shrink and grow as necessary.

Automation Compliance: Application uses primitives’ types (i.e., string, int, float, double, long, char, etc.).

Dual Interface: A custom vtable interface that derives from IDispatch. A dual interface allows a client that can perform vtable binding to call functions in the interface efficiently without losing compatibility with automation controllers that can call only IDispatchmembers.

IDispatch: A COM interface that provides an indirect way to expose object methods and properties. Also known as “Late Binding.”

Microsoft Transaction Server (MTS): A component-based programming model and runtime environment for developing, deploying, and managing high-performance, scalable, and robust enterprise Internet and intranet server applications.

Multiprocess Client: A client that spawns multiple processes, each is performing a specific task on the user machine.

Multi-Use Interfaces: The same instance of the interface/object is used by all clients that instantiate the object.

SafeArrays: The arrays passed by IDispatch::Invoke within VARIANTARGs are called SafeArrays. SafeArrays contain information about the number of dimensions and bounds within them.

Single-process Client: A client that uses a single executable to perform all related tasks on the user machine.

Single-Use Interfaces: A new instance of the interface/object is created when the client instantiates the object.

Structures: A “structure declaration” names a type and specifies a sequence of variable values (called “members” or “fields” of the structure) that can have different types.

Union: A “union declaration” specifies a set of variable values and, optionally, a tag naming the union. The variable values are called “members” of the union and can have different types. Unions are similar to “variant records” in other languages.

Vtable (vtbl): A virtual table containing the addresses (pointers) for the methods and properties of each object in an Automation Server. Of the two implementations of OLE Automation (IDispatch and vtable), vtable is much faster. Also known as “Early Binding.”

COM/DCOM application architecture versus VuGen recording

1. In order for VuGen to trap all necessary COM/DCOM communication between the front-end client application and the backend server, there is a need for the recorder to know about the architecture of the application in terms of COM/DCOM object communication. In general, this information can be gathered with the help of the developer.

2. A general application architecture may look like the following:

On the Client side,

· You might have your own custom data access object.

· You might have different objects talking to the backend server objects and any custom data access object.

On the Server side,

· All DCOM objects created by the client request and communicate with the database for data access.

· In return, these DCOM objects might pass back necessary data to fulfill the client request for data retrieval/update.

3. To let VuGen COM/DCOM recorder know about what objects you are interested in recording, you need to configure the recording option. The purpose of listing all necessary objects here in the filtering option is to enable the recorder to read the type information about the object, so that it would monitor the creation of such objects and all activities done by the objects. Finally, after the recording, the recorder could generate appropriate script for all the activities trapped.

.

Recording options for COM/DCOM

Scripting Language and Option

Script Language

There are four code generation modes

· C Language – Generate Vuser scripts using C. No additional component is required. This mode of code generation is useful for recording applications that use complex COM construct and C++ objects. It is also recommended for users familiar with C programming language.

· Visual Basic for application – Generate Vuser scripts using Visual Basic. Need to have Visual Basic Application installed. This mode of code uses the full capabilities of VB and is ideal for VB based application. It is also recommended for users familiar with Visual Basic.

· Visual Basic Scripting – For VBscript-based applications, such as ASP.

· Java Scripting – For Javascript-based applications such as js files and dynamic HTML applications.

Script Options

1. Basic Options – The Basic script options apply to all of the languages and Vuser types. These options allow you to control the level of detail in the generated script.

a. Close all AUT processes when recording stops: Automatically closes all of the application under test’s (AUT) processes when VuGen stops recording. (disabled by default)

b. Generate fixed think time after end transaction: Add a fixed think time, in seconds, after the end of each transaction. When you enable this option, you can specify a value for the think time. The default is 3 seconds. (disabled by default)

c. Generate recorded events log: Generate a log of all events that take place during recording. (disabled by default)

d. Generate think time greater than threshold: Use a threshold value for think time. If the recorded think time is less than the threshold, VuGen does not generate a think time statement. You also specify the threshold value. The default values is 3—if the think time is less than 3 seconds, VuGen does not generate think time statements. If you disable this option, VuGen will not generate any think times. (enabled by default)

e. Track processes created as COM local servers: Track the activity of the recorded application if one of its sub-processes was created as a COM local server. (disabled by default)

2. Correlation Options – The Correlation options does not apply to C Language scripting. These settings let you configure the extent of automatic correlation performed by VuGen while recording. All correlation options are disabled by default.

a. Correlate small numbers – Correlate short data types such as bytes, characters, and short integers. (disabled by default)

b. Correlate large numbers – Correlate long data types such as integers, long integers, 64-bit characters, float, and double. (disabled by default)

c. Correlate simple strings – Correlate simple, non-array strings and phrases. (enabled by default)

d. Correlate arrays – Track and correlate arrays of all data types, such as string, structures, numbers, etc. (disabled by default)

e. Correlate structures – Track and correlate complex structures. (disabled by default)

COM/DCOM – Filter

DCOM Profile

1. Default Filter: The filter to be used as the default when recording a COM Vuser script.

2. New Filter: A clean filter based on the default environment settings. Note that you must specify a name for this filter before you can record with its settings.

DCOM Listener Settings

In the tree hierarchy of type libraries, you can expand the tree to show all of the available classes in the type library. You can expand the class tree to show all of the interfaces supported by that class.

To exclude a type library, clear the checkbox next to the library name. This excludes all of its classes in that type library. By expanding the tree, you can exclude individual classes or interfaces by clearing the checkbox next to the item.

Various classes can implement an interface differently. When you exclude an interface that is implemented by other classes that have not been excluded, a dialog box opens asking you if you also want to exclude the interface in all classes that implement it this interface.

Note that when you clear the checkbox adjacent to an interface, it is equivalent to selecting it in the Excluded Interfaces dialog box.

1. Environment: The environments to record: ADO objects, RDS Objects, and Remote Objects. Clear the objects you do not want to record.

2. Type Libraries: A type library .tlb file represents the COM object to record. All COM objects have a type library that represents them. You can choose a type library from the Registry, Microsoft Transaction Server, or file system. After adding the Type Libraries, VuGen will display the following information on the lower section of the dialog box:

· TypLib: The name of the type library (the .tlb file).

· Path: The path of the type library.

· Guid: The Global Unique Identifier of the type library.

Additional buttons:

Add: Click to add another COM object. To add a type library from the Registry, select “Browse Registry.” To add a type library from the file system (.tlb, .dll, and so forth.), select “Browse File System.” To add a component from the Microsoft Transaction Server, select “Browse MTS.” To use components from the Microsoft Transaction Server, the computer must have an MTS client installed.

Exclude: Enables you to exclude specific interfaces during recording.

COM/DCOM – Options

DCOM scripting options apply to all programming languages. These settings let you configure the scripting options for DCOM methods and interface handling.

1. ADO Recordset filtering: Condense multiple recordset operations into a single-line fetch statement. (enabled by default)

2. Save Recordset content: Stores Recordset content as grids, to allow viewing of recordset in VuGen. (enabled by default)

3. Generate COM exceptions: Generate COM functions and methods that raised exceptions during recording. (enabled by default)

4. Release COM Objects: Record the releasing of COM objects when they are no longer in use. (disabled by default)

5. Limit size of SafeArray log: Limit the number of elements printed in the safearray log per COM call, to 16. (enabled by default)

6. Generate COM statistics: Generate recording time performance statistics and summary information. (disabled by default)

7. Declare Temporary VARIANTs as Globals: Define temporary VARIANT types as Globals, not as local variables. (disabled by default)

Sample recording against Mercury Interactive’s Flight application

The following is an example of Flight Reservation System (COM sample application that comes with the sample installation).

3. As laid out in the diagram, the GUI (graphical user interface) objects talk to the FRS, which is a COM object that communicates with ADO (Microsoft ActiveX Data Object). ADO as a lower level COM object does all the data access with the backend database and returns Recordset to the FRS object. Then, the GUI objects pick up all actual data from the FRS object for data presentation.

4. In terms of VuGen COM/DCOM recording, there are two approaches.

  1. The first approach is to trap the COM activities generated by the FRS.
  2. The second approach is to trap ALL the ADO activities including the following:

i. Connection to the database

ii. Performing SQL query

iii. Retrieving data via Recordset

Why use ADO

· Considering ADO as a low-level object for data access, it contains all business logic to access the backend database when recording at this level.

· Since Microsoft creates ADO as a standard data access object, VuGen COM/DCom recorder contains extensive support for it because more is known about this object than any others.

· ADO is a thread safe object. The recorded script could be run as a thread.

Why not ADO

· With ADO recording, the recorded script might be lengthy based on the amount of Recordset created and used by the application.

· Extensive correlation might be required to avoid database unique constrain violation when doing update/insert process. A higher-level object might already contain logic to avoid this.

· Very importantly, there might be some other client-side objects that communicate with server-side objects.

Sample recording using ADO

1. Start VuGen and bring up a new COM/DCOM script.

2. On the “Programs to record” field, navigate to the executable of the application. For the sample, please select frsui.exe that is in <Installation >samplesbin.

3. Click on the “Options” button in the lower left-hand corner to set the recording options. You may refer to the “Recording options” section for further information about the settings.

4. Go to the DCOM tab.

i. Select the ADO objects under Environment.

ii. Click on “Add” then on “Browse file system.”

iii. Navigate to “frs.dll” and click OK.

5. After step 4, you should see the “FRS” is automatically added to the type libraries.

6. You can expend “FRS” to select the interface you want to record.

7. Click “OK” to quite the recording options

8. Start recording

NOTE: You will need to correlate the dynamic values so that the script replay will be successful.

How to handle Forms 6 in LoadRunner

How to handle Forms 6 in LoadRunner

1) Record application using latest patch for LR using full trace and recording log.

2) Identify stored procedures used by Forms 6.  In the recording log you can identify them by calls to Forms6_UPI_function.  In the .c files you can identify them because you will see a call to lrd_stmt with a comment starting with “/* select text from sys.all_source where “.  These lines will also include the text “/* INSERT PARAMETERS HERE */”.

You need to identify the owner of the stored procedure, the name of the package in which it occurs, and the name of the procedure itself.

In the recorded script I see there are two stored procedures.  For one of them

lrd_stmt(Csr20, “/* select text from sys.all_source where owner = ‘CIDDEV’ ”

“and name = ‘SEARCH_ENGINE’; */  BEGIN SEARCH_ENGINE.SEARCH_QUERY(/* ”

“INSERT PARAMETERS HERE */); END;”, -1, 0 /*Non deferred*/,

1 /*Dflt Ora Ver*/, 0);

was recorded.  For the other

lrd_stmt(Csr20, “/* select text from sys.all_source where owner = ‘CIDDEV’ ”

“and name = ‘CID_GEN_PCK’; */  BEGIN CID_GEN_PCK.GET_SD_DESCR(/* ”

“INSERT PARAMETERS HERE */); END;”, -1, 0 /*Non deferred*/,

1 /*Dflt Ora Ver*/, 0);

was recorded.

The owner can be identified by the string appearing after “owner =”.

The package name can be identified by the string appearing after “name =”.

The procedure name can be identified by the string appearing after the package name and the “.” afterwards.

First stored procedure:

owner = ‘CIDDEV’

package name = ‘SEARCH_ENGINE’

procedure name = ‘SEARCH_QUERY’

Second stored procedure:

owner = ‘CIDDEV’

package name = ‘CID_GEN_PCK’

procedure name = ‘GET_SD_DESCR’

3) Determine the names and types of all the arguments to the stored procedures that appeared in the generated script.  If they are functions that return a value, what is the type of the return value?  For the other arguments, determine whether they are input-only, output-only, or both input and output.

The type can be a scalar type, such as NUMBER or VARCHAR2, or a compound type such as RECORD of other types or a TABLE of another type.

For Oracle 8.x, you can determine this information for all the procedures in a package by using SQL*Plus and performing DESCRIBE on the owner and package name in SQL*Plus.  You perform DESCRIBE on the owner name, “.”, and the package name.

Example:

SQL> DESCRIBE CIDDEV.SEARCH_ENGINE;

FUNCTION TABLE_RECORD RETURNS TABLE OF RECORD

Argument Name                  Type                    In/Out Default?

—————————— ———————– —— ——–

P1                             TABLE OF RECORD         IN

POUT1                    TABLE OF RECORD         IN/OUT

P2                             VARCHAR2                       OUT

In this example, the function SEARCH_ENGINE.TABLE_RECORD in the CIDDEV schema returns a result that is a TABLE OF RECORD.  It takes 3 arguments, P1, POUT1, and P2.  P1 is an input-only parameter which is a TABLE OF RECORD.  P2 is an input/output parameter of the same type.  P2 is an output parameter of type VARCHAR2.  It is not known from here what type of record is used in the TABLE OF RECORD for P1, POUT1, and the result type of the function.

This information can be obtained by querying the ALL_ARGUMENTS table.  Choose the correct procedure by examining the OWNER, PACKAGE_NAME, and OBJECT_NAME fields.  Show the ARGUMENT_NAME, DATA_TYPE, and IN_OUT fields.  Sort the arguments by POSITION.  Show only top-level arguments by making sure DATA_LEVEL = 0.

Example:

SELECT ARGUMENT_NAME, DATA_TYPE, IN_OUT

FROM ALL_ARGUMENTS

WHERE OWNER = ‘ORAFORMS’ AND PACKAGE_NAME = ‘TABLE_OF_RECORDS’ AND OBJECT_NAME = ‘TABLE_RECORD’ AND DATA_LEVEL = 0

ORDER BY POSITION;

ARGUMENT_NAME                  DATA_TYPE                      IN_OUT

—————————— ——————————           ———

PL/SQL TABLE                    OUT

P1                                           PL/SQL TABLE                    IN

POUT1                                  PL/SQL TABLE                    IN/OUT

P2                                           VARCHAR2                          OUT

This is similar to output produced by describing the stored procedure in SQL*Plus.

SQL> DESCRIBE ORAFORMS.TABLE_OF_RECORDS;

FUNCTION TABLE_RECORD RETURNS TABLE OF RECORD

Argument Name                  Type                                                 In/Out Default?

—————————— ———————–                      —— ——–

P1                                          TABLE OF RECORD           IN

POUT1                                 TABLE OF RECORD           IN/OUT

P2                                          VARCHAR2                          OUT

If the DATA_TYPE is a compound type, like a PL/SQL TABLE or PL/SQL RECORD, it is necessary to determine how the data type is composed of simpler types.  Querying the POSITION, SEQUENCE, DATA_LEVEL, TYPE_OWNER, TYPE_NAME, TYPE_SUBNAME fields in order to obtain this information.

SELECT DATA_LEVEL, POSITION, ARGUMENT_NAME, DATA_TYPE, TYPE_OWNER, TYPE_NAME, TYPE_SUBNAME

FROM ALL_ARGUMENTS

WHERE OWNER = ‘ORAFORMS’ AND PACKAGE_NAME = ‘TABLE_OF_RECORDS’ AND OBJECT_NAME = ‘TABLE_RECORD’

ORDER BY SEQUENCE;

Start of output:

DATA_LEVEL   POSITION ARGUMENT_N DATA_TYPE       TYPE_OWNER TYPE_NAME

———- ———- ———- ————— ———- ———-

TYPE_SUBNA

———-

0          0 [NULL]     PL/SQL TABLE    ORAFORMS   OBJECT

AEXRECTYPE

1          1 [NULL]     PL/SQL RECORD   ORAFORMS   OBJECT

EXRECTYPE

2          1 ID         NUMBER          [NULL]     [NULL]

[NULL]

DATA_LEVEL   POSITION ARGUMENT_N DATA_TYPE       TYPE_OWNER TYPE_NAME

———- ———- ———- ————— ———- ———-

TYPE_SUBNA

———-

2          2 NAME       VARCHAR2        [NULL]     [NULL]

[NULL]

0          1 P1         PL/SQL TABLE    ORAFORMS   OBJECT

AEXRECTYPE

The result type of the function appears first.  It is of type ORAFORMS.OBJECT.AEXRECTYPE, which is a PL/SQL TABLE.  The next row shows that this is a TABLE of EXRECTYPE.  This row also shows that EXRECTYPE is a PL/SQL RECORD.  Note that the DATA_LEVEL of this row is 1, which shows that it is a further description of the information at DATA_LEVEL 0. The next two rows appear at DATA_LEVEL 2, and show that EXECRECTYPE is a RECORD with two fields.  One is ID, which is a NUMBER.  The other is NAME, which is a VARCHAR2.  The following row has DATA_LEVEL 0, which shows that it is a new parameter to the stored procedure.  It is the P1 parameter, which is also of type AEXRECTYPE.

If one of the fields of a RECORD is a string type (VARCHAR2, CHAR, etc.), you can find the size of the field by looking at the DATA_LENGTH column in ALL_ARGUMENTS.

Cautions

a) Oracle allows overloading stored procedures so that more than one stored procedure has the same package name and procedure name as long as the argument types differ.  We are assuming here that the correct instance function can be chosen if a function name can be overloaded.  You can check if a procedure name is overloaded by examining the OVERLOAD field of ALL_ARGUMENTS.  This field will be NULL if the name is not overloaded.  If it is overloaded, then it will be 1 for the first overloaded instance, 2 for the second instance, etc.

SELECT DISTINCT OWNER, PACKAGE_NAME, OBJECT_NAME, OVERLOAD

FROM ALL_ARGUMENTS

WHERE OWNER = ‘CIDDEV’ AND PACKAGE_NAME = ‘SEARCH_ENGINE’ AND OBJECT_NAME = ‘TABLE_RECORD’;

b) Not all Oracle stored procedures are part of a package.  In that case the PACKAGE_NAME is NULL and the OBJECT_NAME gives the name of the procedure in the ALL_ARGUMENTS table.

4) It is necessary to determine the value of the arguments passed to the stored procedure as input and to print out the arguments from the stored procedure after the call so we can correlate queries if necessary.

We can do this by modifying the text of the stored procedure to print out these values.  We only need to do this during recording.  Before trying to replay the script, we will need to restore the stored procedure to its original text.

a)      Find text of stored procedure

You can obtain the text of the body of a package by querying the ALL_SOURCE table for its TEXT field.  Specify the package name by restricting the OWNER and NAME fields.  You can sort the text by its line number, though it appears that it normally is presented in sorted order.

Example:

SELECT TEXT

FROM ALL_SOURCE

WHERE OWNER = ‘QATEST’ AND NAME = ‘MY_MATH’ AND TYPE = ‘PACKAGE BODY’

ORDER BY LINE;

Rows of result:

PROCEDURE MY_SQUARE(X IN NUMBER, Y OUT NUMBER) IS

END MY_MATH;

BEGIN

Y := X * X;

END MY_SQUARE;

END MY_MATH;

You can look at the package declaration instead of the package body by searching for the ‘PACKAGE’ type instead of the ‘PACKAGE BODY’ type.

You can reconstruct the package in SQL*Plus by adding the line

CREATE OR REPLACE

before the text of the package that was returned by this query and adding a line containing

/

after this text.  This is useful because it is necessary to modify the stored procedure in order to record the values of the parameters passed to and from the stored procedure.

Make sure the text of the stored procedure is not truncated if there is a long row.  You might have to redefine the formatting used if a row is truncated.  The following statement in SQL*Plus causes 1000 characters to be printed for each line of TEXT.  This probably is more than enough to avoid truncation.

COLUMN TEXT FORMAT A1000

b)      Modify stored procedure to print arguments

(i) Simple case – procedure with scalar arguments (NUMBER, CHAR, DATE, etc.)

I assume that there is a procedure called TEMP_DUMP that takes a string argument, and somehow writes this argument to disk.  I’ll describe one way of implementing TEMP_DUMP later.

Add calls to TEMP_DUMP at the beginning and end of the stored procedure in order to see the values used by the stored procedure.  Each IN or IN/OUT parameter should be printed at the beginning of the stored procedure.  Each OUT or IN/OUT parameter should be printed at the end of the stored procedure.

For the sample package above, you could add trace statements to get:

CREATE OR REPLACE

PACKAGE BODY MY_MATH IS

PROCEDURE MY_SQUARE(X IN NUMBER, Y OUT NUMBER) IS

BEGIN

TEMP_DUMP(‘BEGIN MY_SQUARE PRELOG’);

TEMP_DUMP(‘X = ‘ || X);

TEMP_DUMP(‘END MY_SQUARE PRELOG’);

Y := X * X;

TEMP_DUMP(‘BEGIN MY_SQUARE POSTLOG’);

TEMP_DUMP(‘Y = ‘ || Y);

TEMP_DUMP(‘END MY_SQUARE POSTLOG’);

END MY_SQUARE;

END MY_MATH;

When MY_MATH.MY_SQUARE(5, :X) was performed this yielded the log

BEGIN MY_SQUARE PRELOG

X = 5

END MY_SQUARE PRELOG

BEGIN MY_SQUARE POSTLOG

Y = 25

END MY_SQUARE POSTLOG

(ii) function

If a function is used instead of a procedure, make sure the function stores the return value in a variable, and print the value of this variable before returning the result.  Treat this variable similar to the way you would treat an OUT parameter.

PACKAGE BODY MY_MATH IS

FUNCTION MY_SQUARE(X IN NUMBER) RETURN NUMBER IS

RETVAL NUMBER;

BEGIN

TEMP_DUMP(‘BEGIN MY_SQUARE PRELOG’);

TEMP_DUMP(‘X = ‘ || X);

TEMP_DUMP(‘END MY_SQUARE PRELOG’);

RETVAL := X * X;

TEMP_DUMP(‘BEGIN MY_SQUARE POSTLOG’);

TEMP_DUMP(‘RETVAL = ‘ || RETVAL);

TEMP_DUMP(‘END MY_SQUARE POSTLOG’);

RETURN (RETVAL);

END MY_SQUARE;

END MY_MATH;

(iii) BOOLEAN type

If a variable is of type BOOLEAN then print its value in a separate IF statement.

IF XBOOL THEN TEMP_DUMP(‘XBOOL  =  TRUE’);

ELSE TEMP_DUMP(‘XBOOL  =  FALSE’);

END IF;

(iv) RECORD type

If the type is a record type, then print out the value of each of the fields of the record.

Example:

PACKAGE MY_MATH IS

TYPE NUMREC IS RECORD (A NUMBER, B NUMBER);

PROCEDURE MY_ADD(X IN OUT NUMREC);

END MY_MATH;

PACKAGE BODY MY_MATH IS

PROCEDURE MY_ADD(X IN OUT NUMREC) IS

BEGIN

TEMP_DUMP(‘BEGIN MY_DOUBLE PRELOG’);

TEMP_DUMP(‘X.A = ‘ || X.A);

TEMP_DUMP(‘X.B = ‘ || X.B);

TEMP_DUMP(‘END MY_DOUBLE PRELOG’);

X.A := X.A + X.B;

TEMP_DUMP(‘BEGIN MY_DOUBLE POSTLOG’);

TEMP_DUMP(‘X.A = ‘ || X.A);

TEMP_DUMP(‘X.B = ‘ || X.B);

TEMP_DUMP(‘END MY_DOUBLE POSTLOG’);

END MY_ADD;

END MY_MATH;

(v) TABLE type

If a TABLE type is used, then it is necessary to print values for each row of the table.

Example:

PACKAGE MY_MATH IS

TYPE NUMTAB IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;

PROCEDURE MY_DOUBLE(X IN OUT NUMTAB);

END MY_MATH;

PACKAGE BODY MY_MATH IS

PROCEDURE MY_DOUBLE(X IN OUT NUMTAB) IS

J BINARY_INTEGER;

BEGIN

TEMP_DUMP(‘BEGIN MY_DOUBLE PRELOG’);

TEMP_DUMP(‘X.FIRST = ‘ || X.FIRST);

TEMP_DUMP(‘X.LAST = ‘ || X.LAST);

FOR J IN X.FIRST .. X.LAST LOOP

TEMP_DUMP(‘X[‘ || J || ‘]=’ || X(J));

END LOOP;

TEMP_DUMP(‘END MY_DOUBLE PRELOG’);

FOR J IN X.FIRST .. X.LAST LOOP

X(J) := X(J) * 2;

END LOOP;

TEMP_DUMP(‘BEGIN MY_DOUBLE POSTLOG’);

TEMP_DUMP(‘X.FIRST = ‘ || X.FIRST);

TEMP_DUMP(‘X.LAST = ‘ || X.LAST);

FOR J IN X.FIRST .. X.LAST LOOP

TEMP_DUMP(‘X[‘ || J || ‘]=’ || X(J));

END LOOP;

TEMP_DUMP(‘END MY_DOUBLE POSTLOG’);

END MY_DOUBLE;

END MY_MATH;

This procedure doubles the values of all elements of a table.  Here is a sample log created for executing this procedure:

BEGIN MY_DOUBLE PRELOG

X.FIRST = 1

X.LAST = 2

X[1]=66

X[2]=55

END MY_DOUBLE PRELOG

BEGIN MY_DOUBLE POSTLOG

X.FIRST = 1

X.LAST = 2

X[1]=132

X[2]=110

END MY_DOUBLE POSTLOG

(vi) If the type is a combination of compound types, such as a TABLE OF RECORD, print out all fields and all rows.  For example create a FOR loop over the rows of the table, and print out every field for each row.

c)       An implementation of dump procedure

There are probably many ways of implementing a dump procedure to print out the values used by the stored procedures.  The following definitions show one way of doing this, though they cause a COMMIT to be performed each time information is logged.  Caution:  this can cause problems if a transaction needs to be rolled back or another transaction is not yet ready to read the changes made by the transaction.

CREATE TABLE TEMP_LOG(ID NUMBER, TEXT VARCHAR2(100));

CREATE SEQUENCE TEMP_SEQUENCE;

CREATE OR REPLACE

PROCEDURE TEMP_DUMP(msg CHAR) IS

BEGIN

INSERT INTO TEMP_LOG values(TEMP_SEQUENCE.nextval, msg);

COMMIT;

END;

To show the log, perform

SELECT TEXT FROM TEMP_LOG ORDER BY ID;

5) Generating code for stored procedure

a)      Determining the statement to be executed

(i) Simple case – procedure with scalar arguments (NUMBER, CHAR, DATE, etc.)

If the stored procedure only takes scalar arguments, then the statement to be executed has a simple form.  The statement is of the form “BEGIN schema.package.procedure(arg1, arg2, …); END;”  For example for the first version of MY_SQUARE presented above the statement would be

BEGIN ORAFORMS.MY_MATH.MY_SQUARE(:X_01, :Y_02); END;

The schema name “ORAFORMS” can be omitted if it is the current schema.

(ii) function

For a function it is necessary to define an auxiliary variable RET_VALUE to store the return value.  This variable is otherwise treated like an output parameter.

For the function version of MY_SQUARE we would get

DECLARE RETVALUE_01 NUMBER;

BEGIN RETVALUE_01 := ORAFORMS.MY_MATH.MY_SQUARE(:X_02); END;

(iii) BOOLEAN type

If a stored procedure takes an argument of type BOOLEAN, pass a value of type INTEGER to the statement.  Declare a local variable of type BOOLEAN.  If the integer value is 0, then use FALSE for the BOOLEAN, else use true.

DECLARE XBOOL_01;

BEGIN

IF :XINT_01 = 0 THEN XBOOL_01 := FALSE;

ELSE XBOOL_01 := TRUE;

END IF;

schema.package.procedure(XBOOL_01);

END;

(iv) RECORD type

If a RECORD type is used for input or output, use separate placeholders for each field of the record.  Use a temporary variable for the record itself.  Copy from the placeholder to the temporary variable for IN parameters.  Copy back to the placeholder for OUT parameters.

DECLARE X NUMREC;

BEGIN

X_00.A := :A_01; X_00.B := :B_02;

MY_MATH.MY_ADD(X_00.A, X_00.B);

:A_01 := X_00.A; :B_02 := X_00.B;

END;

PACKAGE MY_MATH IS

TYPE NUMREC IS RECORD (A NUMBER, B NUMBER);

PROCEDURE MY_ADD(X IN OUT NUMREC);

END MY_MATH;

(v) TABLE type

If a TABLE type is used for an input or output parameter, and the TABLE is a TABLE of ordinary scalar values, then it is not necessary to write anything special for the statement.  Just pass the TABLE as a parameter to the stored procedure.

BEGIN MY_MATH.MY_DOUBLE(:X_01); END;

(vi) TABLE OF RECORD type

If a TABLE OF RECORD is used, then it is represent each field of the TABLE as an array.  Add a temporary variable I of type BINARY_INTEGER to be a loop index. Add a temporary variable to represent the TABLE OF RECORD.  For IN parameters, copy the value of the fields into the TABLE OF RECORD.  Call the stored procedure.  For OUT parameters, copy back the values from the TABLE OF RECORD to the individual fields.

The copying between the TABLE OF RECORD and the individual fields must be in a loop, since it is necessary to copy every single row of the TABLE.  For IN parameters, look at the number of rows passed to the stored procedure during recording, and hard code that into the statement.  For OUT parameters, use the FIRST and LAST functions to determine the size of the TABLE after the call.

Example from TABLE_OF_RECORDS.TABLE_RECORD in section 3).

DECLARE I binary_integer;

P1_01 ORAFORMS.object.aExRecType; POUT1_02 ORAFORMS.object.aExRecType;

RETVALUE_00 ORAFORMS.object.aExRecType;

BEGIN

FOR I IN 1..2 LOOP P1_01(I).ID := :ID_01(I); P1_01(I).NAME := :NAME_02(I); END LOOP;

FOR I IN 1..0 LOOP POUT1_02(I).ID := :ID_03(I); POUT1_02(I).NAME := :NAME_04(I);

END LOOP;

RETVALUE_00 := TABLE_OF_RECORDS.TABLE_RECORD(P1_01, POUT1_02, :P2_05);

FOR I IN POUT1_02.FIRST .. POUT1_02.LAST LOOP

:ID_11(I) := POUT1_03(I).ID; :NAME_12(I) := POUT1_04(I).NAME;

END LOOP;

FOR I IN  RETVALUE_00.FIRST .. RETVALUE_00.LAST LOOP

:ID_06(I) := RETVALUE_00(I).ID;

:NAME_07(I) := RETVALUE_00(I).NAME;

END LOOP;

END;

The TABLE_RECORD function takes three arguments:  P1 an IN parameter of type AEXRECTYPE, POUT1 an IN/OUT parameter of type AEXRECTYPE, and P2 an OUT parameter of type VARCHAR2.  The function returns a value of type AEXRECTYPE.

On input the P1 parameter contained 2 rows, while the POUT parameter contained 0 rows.  The LOOP for POUT for 1..0 can be omitted, since it will never be executed.  We don’t know how many rows will be returned after the procedure, so we loop from RETVALUE_00.FIRST to RETVALUE_00.LAST.

b)      Insert all of the lrd code necessary for replay

In addition to forming the Oracle statement to be executed, other lrd statements must be used.  We will illustrate what needs to be done for the last example, the call to TABLE_OF_RECORDS.TABLE_RECORD, since it shows how to handle a TABLE OF RECORD, one of the more difficult types to handle.

Remove the code that is currently inserted for the stored procedure.  This includes removing calls to lrd_open_cursor and lrd_stmt.

Insert the following in vdf.h.

// Define variable descriptors for P1 parameter.

// Since there are most 2 rows for this TABLE OF RECORD.the 2nd item in these two structure is 2.

// The field size for ID_01_D1 is 23, since VARNUM’s take up 23 bytes.

// The field size for NAME_02_D2 is 27, since there are 25 bytes for the NAME field of AEXRECTYPE plus 2 spare bytes.

static LRD_VAR_DESC                        ID_01_D1 =

{LRD_VAR_DESC_EYECAT, 2, 23, LRD_DBTYPE_ORACLE, {1, 0, 0}, DT_VARNUM};

static LRD_VAR_DESC                        NAME_02_D2 =

{LRD_VAR_DESC_EYECAT, 2, 27, LRD_DBTYPE_ORACLE, {1, 1, 0},

DT_SF_STRIPPED_SPACES};

// Variable descriptors for POUT1 parameter.  Similar to variable descriptors for P1 parameter.

static LRD_VAR_DESC                        ID_03_D3 =

{LRD_VAR_DESC_EYECAT, 2, 23, LRD_DBTYPE_ORACLE, {1, 0, 0}, DT_VARNUM};

static LRD_VAR_DESC                        NAME_04_D4 =

{LRD_VAR_DESC_EYECAT, 2, 27, LRD_DBTYPE_ORACLE, {1, 1, 0},

DT_SF_STRIPPED_SPACES};

// Variable descriptor for P2 scalar parameter.  Since it is a VARCHAR2 field, and string length is 10, since at most 9 characters are found in this string + one character for terminating null character.

// Second item in structure is 1, since this is a scalar.

static LRD_VAR_DESC                        P2_05_D5 =

{LRD_VAR_DESC_EYECAT, 1, 10, LRD_DBTYPE_ORACLE, {1, 1, 0},

DT_SF_STRIPPED_SPACES};

// Variable descriptors for return value fields.

static LRD_VAR_DESC                        ID_06_D6 =

{LRD_VAR_DESC_EYECAT, 2, 23, LRD_DBTYPE_ORACLE, {1, 0, 0}, DT_VARNUM};

static LRD_VAR_DESC                        NAME_07_D7 =

{LRD_VAR_DESC_EYECAT, 2, 25, LRD_DBTYPE_ORACLE, {1, 1, 0},

DT_SF_STRIPPED_SPACES};

// Declare statement handle for stored procedure call

static void FAR *           OraStm0;

// Declare bind handles for placeholders involving stored procedure call

static void FAR *           OraBnd01;

static void FAR *           OraBnd02;

static void FAR *           OraBnd03;

static void FAR *           OraBnd04;

static void FAR *           OraBnd05;

static void FAR *           OraBnd06;

static void FAR *           OraBnd07;

Insert the following in the .c file.

// General format

// lrd_ora8_handle_alloc to allocate statement handle

// lrd_ora8_stmt to prepare statement

// For each value assigned to a placeholder, use lr_assign

// For each placeholder, lr_ora8_bind_placeholder

// lrd_ora8_exec to execute statement containing stored procedure

// lrd_ora8_handle_free to free statement handle

// Environment handle OraEnv0 and service handle OraSvc0 must be defined previously.

// Use the current environment and service handles here instead of the names given below.

lrd_ora8_handle_alloc(OraEnv0, STMT, &OraStm0, 0);

lrd_ora8_stmt(OraStm0, /* statement from 5) a) (vi) above */, 1, 0, 0);

// Use an lrd_assign for each row of the field in an input array.  First ID field of P1 parameter.

lrd_assign(&ID_01_D1, “11”, 0, 0, 0);       // ID of 1st record is 11.  Could be seen from log of step 4).

lrd_assign(&ID_01_D1, “12”, 0, 1, 0);       // ID of 2nd record is 12.

// Use LRD_BIND_AS_ARRAY for type of lrd bind.

lrd_ora8_bind_placeholder(OraStm0, &OraBnd01, “ID_01”, &ID_01_D1, LRD_BIND_AS_ARRAY, 0);

// Do the same for NAME field of P1 parameter

lrd_assign(&NAME_02_D2, “Markn1”, 0, 0, 0);   // NAME of 1st record is “Markn1”

lrd_assign(&NAME_02_D2, “Markn2”, 0, 1, 0);   // NAME of 2nd record is “Markn2”

lrd_ora8_bind_placeholder(OraStm0, &OraBnd02, “NAME_02”, &NAME_02_D2, LRD_BIND_AS_ARRAY, 0);

// Binds for IN OUT parameter POUT2

// Since there were 0 rows for it on input, no lrd_assign’s are necessary

lrd_ora8_bind_placeholder(OraStm0, &OraBnd03, “ID_03”, &ID_03_D3, LRD_BIND_AS_ARRAY, 0,            0);

lrd_ora8_bind_placeholder(OraStm0, &OraBnd04, “NAME_04”, &NAME_04_D4, LRD_BIND_AS_ARRAY, 0, 0);

// P2 is a scalar parameter (NUMBER).  Use 0 as lrd bind type, not LRD_BIND_AS_ARRAY.

// If lrd_assign were called here, 2nd to last argument would be 0, since scalar type used.

// However, since P2 is an OUT parameter, no lrd_assign is needed.

lrd_ora8_bind_placeholder(OraStm0, &OraBnd05, “P2_05”, &P2_05_D5, 0, 0, 0);

// Binds for value returned by stored procedure.

lrd_ora8_bind_placeholder(OraStm0, &OraBnd06, “ID_06”, &ID_06_D6, LRD_BIND_AS_ARRAY, 0, 0);

lrd_ora8_bind_placeholder(OraStm0, &OraBnd07, “NAME_07”, &NAME_07_D7, LRD_BIND_AS_ARRAY, 0, 0);

lrd_ora8_exec(OraSvc0, OraStm0, 1, 0, &uliRowsProcessed, 0, 0, 0, 0, 0);

lrd_handle_free(&OraStm0, 0);

How to record and replay against Oracle Web Application 11i

How to record and replay against Oracle Web Application 11i

Introduction

Starting with LoadRunner8.0, LoadRunner comes with a Web GUI-level recording solution for Oracle Web Applications 11i. The recorder for this solution is able to create an object oriented script that accurately interprets Javascript on the Web pages commonly found on Oracle Web Applications 11i.

When you record a session, VuGen records all of the activity and creates a script. During replay, the script emulates the HTTP protocol communication between your browser and the server. This script can intuitively represent actions in the Oracle interface.

Requirements

1. LoadRunner 8.0 or higher

2. License for the ‘OracleWebJS’ Vuser

Supported Oracle Applications 11i

Following are the officially supported Oracle Application 11i:

• HR

• Financials

• IStore

• Marketing and Sales

Note: Even though not listed, there is a good chance that other Oracle Applications 11i will work with the solution. Mercury strongly encourages you to try it out if situation allows.

Supported OS

· Recording and Replay under Windows on IE only

· Replay under Unix – Solaris, Linux, HP and IBM

· If NCA functions are present then Linux platform is not supported.

Recording Oracle Web Application 11i script

1. From the ‘New Multiple Protocol Script’ window, add ‘Oracle Web Application 11i’ and click OK.

Note: Creating script with Multiple Protocol recorder provides the option to regenerate the script after recording (if necessary).

2. Set the following Recording Options:

a. Internet Protocol: Recording:

· Select GUI- based script’

· Click on ‘GUI Advanced’ and enable ‘Use HTML-based script as fallback’

· Click on ‘GUI Advanced’ and enable ‘Enable out-of-context recording

Note: You can refer to the books online for the detail about these options.

  1. Internet Protocol: Correlation:

a. Make sure that you have ‘Enable Correlation during recording’

b. Make sure that ‘Oracle’ is selected. You can expand the list to see the details about each rule if you wish.

  1. All the other recording options are optional. Modify them if necessary to fit your test requirement. Otherwise, leave them as default.

Replaying Oracle Web Application 11i script

With the Oracle Web Application 11i script, Mercury is offering a zero correlation solution. This means that you do not have to worry about handling the dynamic values such as session ID being recorded into the script. After recording, you can proceed to enhance the script directly.

Apart from the regular functionality (transaction, rendezvous, parameterization, and etc…), there is also a new function to help you to evaluate a JavaScript or DOM objects if desired. The function, web_eval_java_script has three usages:

  • To evaluate JavaScript
  • To evaluate the DOM object Expression and save the value
  • To dump JavaScript properties of the window object

This function is never recorded. You can add it to your script to handle situations for which there is no standard solution.

For cache simulation, there is a new feature that allows you to fully simulate disk caching. You will use the web_dump_cache and web_load_cache function to accomplish the task. The idea is to capture the browser cache once, while running the script manually. Then the same cache can be used repeatedly in tests. Using Vuser persistent caching improves CPU usage on the application and database servers.

Tips and Tricks

1. Recording a script will fail if the machine is not restarted after installation or if an error occurs while restarting

2. Dynamic link texts (e.g., web_text_link(…, “Text=J Novak” , …) when “J Novak” is the first name in a search in a data base ) are not supported – Need to manually use Ordinal instead or use web_reg_save_param to do correlation.

3. To iterate the script, recording of an Action section needs to end at the same point it starts

4. While recording use only

  1. Elements inside the browser (‘Favorites’ and ‘home’ buttons are not supported)
  2. Direct navigation using the address bar

5. JavaScript alert (Pop-up window) is not considered an error as it might be informational. If needed, you can add a ContentCheck rule in the Run-Time Settings. You can see the alert message in the execution log and also the Run-Time Browser.

You can add verifications with web_eval_java_script().

Appendix

How to regenerate the script after recording

If needed, you can always regenerate the script after recording if the script is generated using the multiple protocol recorder. To regenerate the script,

1. Go to Tools à Regenerate Vusers.

2. Click on "Options" to set the option (you can change it if you want), and click <OK> to save the option.

3. Click <OK> to regenerate the script.

Note: You can regenerate the script to the regular HTML or URL mode if desired.

Steps to enable disk caching

The following function will help to enable disk caching. This is an optional step which allows you to Save the Vuser cache to a file. You will need to first dump the information to a cache file using web_dump_cache. During replay, each Vuser calls this information using web_load_cache functions. Here are the steps:

1. Insert the web_dump_cache function at the beginning of your script. This function creates a cache file in the location specified in the FileName argument.

Example:

To create a cache file in C:temp for each VuserName parameter:

web_dump_cache("paycheckcache","FileName=c:\temp\{VuserName}paycheck", "Replace=yes", LAST)

If you run a single Vuser user ten times, VuGen creates ten cache files in the following format, where the prefix is the VuserName value:

Ku001paycheck.cache
Ku002paycheck.cache
Ku003paycheck.cache

2. Run the script at least once.

3. Insert the web_load_cache function into your script, before the Vuser actions. This function loads a cache file whose location is specified in the FileName argument.

Example: To load the file created on step1:

web_load_cache("ActionLoad","FileName=c:\temp\{VuserName}paycheck", LAST);

4. Comment out the web_dump_cache function.

5. Run and save the script.

Function reference and example for web_eval_java_script function

web_eval_java_script function has three usages, each with a different syntax. Do not mix arguments between syntaxes:

Purpose Function
To evaluate JavaScript Syntax:

int web_eval_java_script ( const char *stepName, const char *script, [DESCRIPTION, const char *arg1, …, const char *argn,] LAST );

The second argument, script, is the JavaScript expression to be evaluated.

Example:

The following will run the script method, document.getElementByID.

web_eval_java_script("Get My ID",

"Script=document.getElementById(‘MyID’).innerText = ‘MyText’;",

LAST);

To evaluate the DOM object Expression and save the value Syntax:

int web_eval_java_script ( const char *stepName, const char *Expression, const char *SaveExpressionResult, [DESCRIPTION, const char *arg1, …, const char *argn,] LAST );

The second argument, Expression, is the DOM expression to be evaluated. Upon execution, it will evaluate the DOM object Expression and places the DOM object’s printable value in SaveExpressionResult. Non-printable values such as "Object"/"void"/"NULL"/"Array" will return the value name.

Example:

The following will get the document title and stores it in a parameter.

web_eval_java_script("Get Title",

"Expression=top.document.title",

"SaveExpressionResult=prm",

LAST);

lr_output_message(lr_eval_string("{prm}"));

To dump JavaScript properties of the window object Syntax:

int web_eval_java_script ( const char *stepName, const char * PrintProperties, [DESCRIPTION, const char *arg1, …, const char *argn,] LAST );

The second argument, PrintProperties, is used to dump the JavaScript properties of the window object. Enter the argument: "PrintProperties=True"

Example:

The following will print the JavaScript property.

web_eval_java_script("Print data",

"PrintProperties=True ",

LAST);

GUI-Level Vuser Functions

Refer to the function help references for additional information about these functions

How to capture the URL that is formed because of redirection?

How to capture the URL that is formed because of redirection?

Example:

When visiting http://www.mercury.com, user will be redirected to a different location. How to capture the redirected URL.

Solution

Use web_reg_save_param()

When that the server redirects a HTTP request to another URL, it normally does that by sending a HTTP 302 Moved header. To verify

1. Run the script in extended log with "data retuned by server’ turned on

2. Check the execution log for "302 moved’

You can also see that along with this header, the server also sends the following header information:

"Location: <URL>."

The URL specified in the "Location’ header will be the URL that the original HTTP request be redirected to. To capture this URL, you have to capture the string between "Location:" and "\r\n" (end of line).

Example:

Action()

{

//use the correlation statement to capture the redirection

web_reg_save_param ("Redirection", "LB=Location: ","RB=\r\n" ,LAST );

//Visit Mercury homepage

web_url("Mercury","URL=http://www.mercury.com/", LAST);

//Print the redirected URL

lr_message("redirected address = %s", lr_eval_string("{Redirection}"));

return 0;

}