org.testeditor.fixture.host.HostDriverFixture.java Source code

Java tutorial

Introduction

Here is the source code for org.testeditor.fixture.host.HostDriverFixture.java

Source

/*******************************************************************************
 * Copyright (c) 2012 - 2018 Signal Iduna Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Signal Iduna Corporation - initial API and implementation
 * akquinet AG
 * itemis AG
 *******************************************************************************/
package org.testeditor.fixture.host;

import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testeditor.fixture.core.FixtureException;
import org.testeditor.fixture.core.MaskingString;
import org.testeditor.fixture.core.TestRunListener;
import org.testeditor.fixture.core.TestRunReportable;
import org.testeditor.fixture.core.TestRunReporter;
import org.testeditor.fixture.core.TestRunReporter.Action;
import org.testeditor.fixture.core.TestRunReporter.SemanticUnit;
import org.testeditor.fixture.core.interaction.FixtureMethod;
import org.testeditor.fixture.core.logging.FilenameHelper;
import org.testeditor.fixture.host.locators.LocatorByStart;
import org.testeditor.fixture.host.locators.LocatorByStartStop;
import org.testeditor.fixture.host.locators.LocatorByWidth;
import org.testeditor.fixture.host.locators.LocatorStrategy;
import org.testeditor.fixture.host.net.Connection;
import org.testeditor.fixture.host.s3270.Result;
import org.testeditor.fixture.host.s3270.Status;
import org.testeditor.fixture.host.s3270.actions.Command;
import org.testeditor.fixture.host.s3270.actions.ControlCommand;
import org.testeditor.fixture.host.s3270.options.CharacterSet;
import org.testeditor.fixture.host.s3270.options.TerminalMode;
import org.testeditor.fixture.host.s3270.options.TerminalType;
import org.testeditor.fixture.host.s3270.statusformat.FieldProtection;
import org.testeditor.fixture.host.s3270.statusformat.ScreenFormatting;
import org.testeditor.fixture.host.screen.Field;
import org.testeditor.fixture.host.screen.Offset;
import org.testeditor.fixture.host.screen.TerminalScreen;
import org.testeditor.fixture.host.util.LineReader;
import org.testeditor.fixture.host.util.Timer;

/**
 * HostDriverFixture provides convenience methods for automating mainframe
 * functional tests. It consists of an API to deal with mainframe fields in
 * screens. As a test driver, we use the X3270 framework to communicate with the
 * mainframe which should be tested.
 *
 * @see <a href="http://x3270.bgp.nu/">http://x3270.bgp.nu/</a>
 */
public class HostDriverFixture implements TestRunListener, TestRunReportable {

    private static final int THREAD_SLEEP_IN_MILLIS = 100;

    // Maximum amount of time x 100 ms -> 100 x 100ms = 10.000ms = 10 Seconds
    private static final int MAX_TIME_TO_WAIT = 100;

    private static final Logger logger = LoggerFactory.getLogger(HostDriverFixture.class);

    private Connection connection;
    private TerminalMode terminalMode = TerminalMode.MODE_24x80;
    private String pathName = "./screenshots";
    private String type = "html";
    private FilenameHelper filenameHelper = new FilenameHelper();
    private String runningTest = null;
    private Offset offset;
    private Command commandAscii = new Command("Ascii", "Ascii");
    private Timer timer = new Timer(TimeUnit.MILLISECONDS);

    public HostDriverFixture() {
        this.connection = new Connection();
    }

    public HostDriverFixture(Connection connection) {
        this.connection = connection;
    }

    /**
     * The Connection is responsible for the subprocess of the s3270/ws3270
     * application. The whole communication will be done through this
     * subprocess.
     *
     * @param s3270Path
     *           The path to the s3270/ws3270 application.
     * @param hostname
     *            The hostname to be connected to.
     * @param port
     *            The port of the host to be connected to.
     * @param offsetRow
     *            The offsetRow for the screen origin. Default for the x3270
     *            driver is that the row and column are zero-origin (0;0), that
     *            means the begin point is at row 0 and column 0. The offsetRow
     *            will rearrange the startpoint around the value for the row .
     * @param offsetColumn
     *            The offsetColumn for the screen origin. Default for the x3270
     *            driver is that the row and column are zero-origin (0;0), that
     *            means the begin point is at row 0 and column 0. The
     *            offsetColumn will rearrange the startpoint around the value
     *            for the column.
     * @param verifyCertificate 
     *            This boolean value can be true or false. 
     *            <p>If <b><i>true</i></b> is chosen, several environment variables on test system can be set as follows:
     *            <br>HOSTFIXTURE_CADIR : Specifies a directory containing CA (root) certificates to use when verifying a certificate provided by the host. (OpenSSL only). 
     *            <br>HOSTFIXTURE_CAFILE : Specifies a PEM-format file containing CA (root) certificates to use when verifying a certificate provided by the host. (OpenSSL only). 
     *            <br>HOSTFIXTURE_CERTFILETYPE : Specifies the type of the certificate file specified by -certfile. Type can be pem or asn1. (OpenSSL only) 
     *            <br>HOSTFIXTURE_CERTFILE : Specifies a file containing a client certificate to provide to the host. The default file type is PEM.   
     *            <br>HOSTFIXTURE_CHAINFILE : Specifies a certificate chain file in PEM format, containing a certificate to provide to the host,
     *            as well as one or more intermediate certificates and the CA certificate used to sign that certificate. 
     *            If -chainfile is specified, it overrides -certfile. (OpenSSL only) 
     *            <br>HOSTFIXTURE_CLIENTCERT : Specifies the name of a client certificate to provide to the host. (MacOS only) 
     *            <br>HOSTFIXTURE_KEYFILE : Specifies a file containing the private key for the certificate file (specified via -certfile or -chainfile). The default file type is PEM. (OpenSSL only)
     *            <br>HOSTFIXTURE_KEYFILETYPE : Specifies the type of the private key file specified by -keyfile. Type can be pem or asn1. (OpenSSL only) 
     *            <br>HOSTFIXTURE_KEYPASSWD : Specifies the password for the private key file (OpenSSL) or client certificate file (MacOS), 
     *            if it is encrypted. The argument can be file:filename, specifying that the password is in a file, 
     *            or string:string, specifying the password on the command-line directly. 
     *            If the private key file is encrypted and no -keypasswd option is given, secure connections will not be allowed.  
     *            <p> If <b><i>false</i></b> is chosen, the connection is established with the parameter <b>"noverifycert"</b> this is for SSL/TLS connections, 
     *            that do not verify the host certificate.
     *            <p> For further Connection options see <a href="http://x3270.bgp.nu/s3270-man.html">s3270 Options</a>       
     * @throws FixtureException 
     *            A FixtureException is thrown when the connection to the host fails.        
     */
    @FixtureMethod
    public Connection connect(String s3270Path, String hostname, int port, int offsetRow, int offsetColumn,
            boolean verifyCertificate) throws FixtureException {
        logger.debug("Host-Fixture connecting to host '{}' on port '{}'with offsetRow {} and offsetColumn {} ...",
                hostname, port, offsetRow, offsetColumn);
        this.offset = new Offset(-offsetRow, -offsetColumn);
        logger.trace("Offset to original s3270 screen X-Position = {} ; Y-Position = {}.", offsetColumn, offsetRow);
        TerminalType type = TerminalType.TYPE_3279;
        CharacterSet charSet = CharacterSet.CHAR_GERMAN_EURO;
        logger.trace(
                "Used parameters for connection:\n Terminal Type : {} ;\n Character Set : {} ;\n Path to s3270 application : {} ;"
                        + "\n Connected Hostname : {} ;\n Connected Port : {}; \n Verify Certificate : {};",
                type, charSet, s3270Path, hostname, port, verifyCertificate);

        timer.startTimer();
        Connection hostConnection = connection.connect(s3270Path, hostname, port, type, terminalMode, charSet,
                offset, verifyCertificate);
        if (connection.isConnected()) {
            waitUntilScreenIsFormatted(MAX_TIME_TO_WAIT);
            timer.stopTimer();
            logger.trace("Elapsed time in millis after connect: {}", timer.getElapsedTime());
            logger.debug("successfully connected to host='{}', port='{}'", hostname, port);
            return hostConnection;
        } else {
            throw new FixtureException(
                    "The connection to host '" + hostname + "' on port '" + port + "' could not be established.",
                    FixtureException.keyValues("hostname", hostname, "port", port));
        }
    }

    @VisibleForTesting
    void waitUntilScreenIsFormatted(int maxIterationThreadSleeping) {
        Status status = connection.getStatus();
        ScreenFormatting screenFormatting = status.getScreenFormatting();
        logger.trace("Screenformatting : {}", screenFormatting.getFormatting());
        Integer i = 0;
        while (status.getScreenFormatting() != ScreenFormatting.FORMATTED && i++ < maxIterationThreadSleeping) {
            performWaits(status);
        }
    }

    protected void performWaits(Status status) {
        try {
            TimeUnit.MILLISECONDS.sleep(THREAD_SLEEP_IN_MILLIS);
        } catch (InterruptedException e) {
            logger.error("Something went wrong during wait period: {}", e.getMessage());
        }
        logger.trace("waited {} ms", THREAD_SLEEP_IN_MILLIS);
        status = connection.getStatus();
    }

    /**
     * Disconnects from host.
     * {@link org.testeditor.fixture.host.net.Connection#disconnect()}
     *
     */
    @FixtureMethod
    public boolean disconnect() throws FixtureException {
        logger.debug("Disconnecting ...");
        return connection.disconnect();
    }

    /**
     * Provides the Status Objekt which will be returned when an input or action
     * is executed.
     *
     * @return {@link org.testeditor.fixture.host.s3270.Status}
     */
    @FixtureMethod
    public Status getStatus() {
        logger.debug("get Status ...");
        return connection.getStatus();
    }

    /**
     * Types a given value into a given field with x and y coordinates as elementlocator.
     * @param elementLocator
     *            A String representation of a point to set the cursor on. <br>
     *            Example: elementLocator = "1;2" (for the
     *            {@link LocatorStrategy#START}<br>
     *            1 is the ROW representation and 2 is the COLUMN
     *            representation.
     * @param locatorType
     *          Type of locator for Gui-Widget as enum {@link LocatorStrategy}. This can be START_STOP or just START.
     *          START_STOP means a range of a field with a beginning point and an end point. 
     *          START means only the beginning point is given.. 
     * @param value
     *            The String which should be entered at the given position under
     *            elementLocator
     * @throws FixtureException 
     *            When an input field is protected so the user can not reach this field to type something into.
     */
    @FixtureMethod
    public void typeInto(String elementLocator, LocatorStrategy locatorType, String value) throws FixtureException {
        Result result = moveCursor(elementLocator, locatorType);
        result.logStatus();
        Status status = result.getStatus();
        if (status.getFieldProtection() == FieldProtection.UNPROTECTED) {
            waiting(100);
            Command commandType = new Command("String", "String(\"" + value + "\")");
            connection.doCommand("String(\"" + value + "\")", commandType);
            // just to see if typed in successfully.
            connection.doCommand("ascii", commandAscii);
        } else {
            int currentCursorColumn = status.getCurrentCursorColumn();
            int currentCursorRow = status.getCurrentCursorRow();
            throw new FixtureException(
                    "The field at the position x = '" + currentCursorColumn + "' and y = '" + currentCursorRow
                            + "' is protected.",
                    FixtureException.keyValues("X-Position", currentCursorColumn, "Y-Position", currentCursorRow));
        }
    }

    /**
     * Types given text not obfuscated into a non secure input field
     * on a specified Gui-Widget. The specialty about the text which is typed in is, 
     * that it will be not obfuscated in log files
     * 
     * @param elementLocator 
     *          Locator for Gui-Widget as {@link String}.
     * @param locatorType 
     *          Type of locator for Gui-Widget as enum {@link LocatorStrategy}. This can be START_STOP or just START.
     *          START_STOP means a range of a field with a beginning point and an end point. 
     *          START means only the beginning point is given.. 
     * @param value 
     *          A masked {@link String} which is set into the textfield.
     * @throws FixtureException 
     *          When an input field is protected so the user can not reach this field to type something into.
        
     */
    @FixtureMethod
    public void typeConfidentialIntoInsecure(String elementLocator, LocatorStrategy locatorType, //
            MaskingString value) throws FixtureException {
        Result result = moveCursor(elementLocator, locatorType);
        result.logStatus();
        Status status = result.getStatus();
        if (status.getFieldProtection() == FieldProtection.UNPROTECTED) {
            waiting(100);
            Command commandType = new Command("String", "String(\"" + value + "\")");
            connection.doCommand("String(\"" + value.get() + "\")", commandType);
            // just to see if typed in successfully.
            connection.doCommand("ascii", commandAscii);
        } else {
            throw new FixtureException(
                    "The field at the position x = '" + status.getCurrentCursorColumn() + "' and y = '"
                            + status.getCurrentCursorRow() + "' is protected.",
                    FixtureException.keyValues("xPosition", status.getCurrentCursorColumn(), "yPosition",
                            status.getCurrentCursorRow()));
        }
    }

    /**
     * Provides the possibility to access some special commands defined under
     * {@link ControlCommand}
     * 
     * 
     * @param value
     *            a ControlCommand like {@link ControllCommand#DELETE},
     *            {@link ControllCommand#TAB}, {@link ControllCommand#ENTER}
     *            etc.
     * 
     * @see ControlCommand
     */
    @FixtureMethod
    public void sendCommand(ControlCommand command) throws FixtureException {
        waiting(100);
        Command commandString = new Command(command.getCommand(), command.getCommand());
        connection.doCommand(command.getCommand(), commandString);
        logger.trace("Command : '{}' sent to 3270-Emulator", commandString);
        connection.doCommand("ascii", commandAscii);
    }

    /**
     * Reads a value on a given position range start(x,y) and end(x,y)
     * 
     * @param elementLocator
     *            The elementlocator exists of position definitions with a
     *            starting(row,column) and end(row,column) position in the form
     *            "2;12;5;16" which means the start position is at row = 2,
     *            column = 12 up to stop position row = 5, column 16.
     * @param locatorType
     *            see {@link LocatorStrategy} this is the type how the
     *            elementlocator is defined.
     * @return the String value which is read at the given position.
     */
    @FixtureMethod
    public String readValueAt(String elementLocator, LocatorStrategy locatorType) throws FixtureException {
        logger.debug("Read value at position {} by {}", elementLocator, locatorType);
        return getElement(elementLocator, locatorType);
    }

    protected String getElement(String elementLocator, LocatorStrategy locatorType) throws FixtureException {
        logger.trace("Lookup element {} type {}", elementLocator, locatorType.name());
        String result = null;
        Status status = getStatus();
        try {
            switch (locatorType) {
            case START_STOP:
                result = getValueByStartStop(new LocatorByStartStop(elementLocator, status, offset));
                break;
            case WIDTH:
                result = getValueByWidth(new LocatorByWidth(elementLocator, status, offset));
                break;
            default:
                result = getValueByStartStop(new LocatorByStartStop(elementLocator, status, offset));
                break;
            }

        } catch (Exception e) {
            throw new FixtureException(e.getMessage(), FixtureException.keyValues("elementlocator", elementLocator,
                    "locatorType", locatorType, "offset", offset), e);
        }
        return result;
    }

    private String getValueByStartStop(LocatorByStartStop locator) {
        Command commandAscii = new Command("Ascii", "Ascii");
        Result result = connection.doCommand("ascii", commandAscii);
        List<String> dataLines = result.getDataLines();
        Status status = result.getStatus();
        StringBuffer sb = new StringBuffer();
        // When only one row is available
        if (locator.getStartRow() == locator.getEndRow()) {
            String line = dataLines.get(locator.getStartRowWithOffset());
            line = LineReader.extracted(line);
            if (LineReader.extracted(line).length() > status.getNumberColumns()) {
                throw new RuntimeException("Row: " + line + " is greater than the specified max column size "
                        + status.getNumberColumns());
            }
            LineReader lineReader = new LineReader();
            sb.append(lineReader.readSingleLine(line, locator));
        } else {
            // When multiple lines are available
            LineReader lineReader = new LineReader();
            sb.append(lineReader.readMultilines(dataLines, locator));
        }
        return sb.toString();
    }

    private String getValueByWidth(LocatorByWidth locator) {
        Command commandAscii = new Command("Ascii", "Ascii");
        Result result = connection.doCommand("ascii", commandAscii);
        Status status = result.getStatus();
        List<String> dataLines = result.getDataLines();
        String line = dataLines.get(locator.getStartRow() + offset.getOffsetRow());
        if (LineReader.extracted(line).length() > status.getNumberColumns()) {
            throw new RuntimeException(
                    "Row: " + line + " is greater than the specified max column size " + status.getNumberColumns());
        }
        LineReader lineReader = new LineReader();
        return lineReader.readSingleLineWidth(line, locator);
    }

    private void waiting(long milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException ex) {
            logger.error(ex.getMessage());
        }
    }

    private Result setCursorPosition(int row, int col) {
        logger.trace("Set cursor position to x = {}, y = {}", col, row);
        Command command = new Command("MoveCursor",
                "MoveCursor(" + (row - offset.getOffsetRow()) + "," + (col - offset.getOffsetColumn()) + ")", row,
                col);
        return connection.doCommand("MoveCursor(" + row + "," + col + ")", command);
    }

    /**
     * @param elementLocator
     *            A String representation of a point to set the cursor on. <br>
     *            Example: elementLocator = "1;2" (for the
     *            {@link LocatorStrategy#START}<br>
     *            1 is the ROW representation and 2 is the COLUMN
     *            representation.
     * @param locatorType
     *            see {@link LocatorStrategy}
     * @return {@link Result} of the action.
     * @throws FixtureException 
     */
    @FixtureMethod
    public Result moveCursor(String elementLocator, LocatorStrategy locatorType) throws FixtureException {
        logger.debug("Move cursor to position {} by {}.", elementLocator, locatorType);
        Result result = null;
        Status status = getStatus();
        try {
            switch (locatorType) {
            case START:
                LocatorByStart locatorByStart = new LocatorByStart(elementLocator, status, offset);
                result = setCursorPosition(locatorByStart.getStartRowWithOffset(),
                        locatorByStart.getStartColumnWithOffset());
                break;
            case START_STOP:
                LocatorByStartStop locatorByStartStop = new LocatorByStartStop(elementLocator, status, offset);
                result = setCursorPosition(locatorByStartStop.getStartRowWithOffset(),
                        locatorByStartStop.getStartColumnWithOffset());
                break;
            case WIDTH:
                LocatorByWidth locatorByWidth = new LocatorByWidth(elementLocator, status, offset);
                result = setCursorPosition(locatorByWidth.getStartRowWithOffset(),
                        locatorByWidth.getStartColumnWithOffset());
                break;
            }

        } catch (Exception e) {
            throw new FixtureException(e.getMessage(), FixtureException.keyValues("elementlocator", elementLocator,
                    "locatorType", locatorType, "offset", offset), e);
        }
        return result;
    }

    /**
     * Prints all {@link Field}s of a mainframe ScreenBuffer in the form:
     * 
     * <pre>
     * |----------------------------------------------------------------------------------------------------------------------------------------------|
     * |Nr |row|field|width|colStart|colEnd|numeric|protected|hidden|value                                                                           
     * |----------------------------------------------------------------------------------------------------------------------------------------------|
     * |1  |0  |1    |4    |1       |4     |       |true     |false | 'z/OS'                                                                         
     * |2  |0  |2    |10   |6       |15    |       |false    |false | ' Z18 Level'                                                                   
     * |3  |0  |3    |5    |17      |21    |true   |false    |false | ' 0609'                                                                        
     * |4  |0  |4    |31   |23      |53    |       |false    |true  | '                               '                                              
     * |5  |0  |5    |24   |55      |78    |       |false    |false | 'IP Address = 78.51.59.98'                                                     
     * |6  |0  |6    |6    |80      |85    |       |false    |false | '
     * ...
     * </pre>
     * 
     * <strong>Caution:</strong> But before it will be verified, if the result
     * of ScreenBuffer is formatted. If the application program uses field
     * attributes to define fields on the screen, then the screen is formatted.
     * If there are no fields defined on the screen, then the screen is
     * unformatted, and the operator uses it in free-form manner.
     * 
     * @return The String reprentation of all found {@link Field}s in
     *         ScreenBuffer.
     */
    @FixtureMethod
    public String buildAllFieldsAsString() throws FixtureException {
        logger.debug("Generate all 3270-Fields on screen as String ...");
        String allFieldAsString = null;
        Command command = new Command("ReadBuffer", "ReadBuffer(Ascii)");
        Result result = connection.doCommand("ReadBuffer(Ascii)", command);
        Status status = result.getStatus();
        ScreenFormatting screenFormatting = status.getScreenFormatting();
        // Check if ScreenBuffer is formatted !
        int maxWaitCounter = 0;
        while (screenFormatting != ScreenFormatting.FORMATTED) {
            waiting(500);
            maxWaitCounter++;
            logger.trace("waiting 500 ms ...");
            result = connection.doCommand("ReadBuffer(Ascii)", command);
            status = result.getStatus();
            screenFormatting = status.getScreenFormatting();
            // Wait maximal 30 seconds that screen is formatted
            if (maxWaitCounter > 60) {
                break;
            }
        }
        if (screenFormatting == ScreenFormatting.FORMATTED) {
            TerminalScreen screen = new TerminalScreen();
            allFieldAsString = screen.printAllFieldAsString(result.getDataLines());
            logger.trace("All fields on screen \n {}", allFieldAsString);

        } else {
            throw new FixtureException("The result of delivered Host-Screen is not formatted, "
                    + "so it is not possible to get any information about fields!");
        }
        return allFieldAsString;
    }

    private void takeScreenshot(String filenameBase) {
        String testcase = getCurrentTestCase();
        String filename = filenameHelper.constructFilename(pathName, testcase, filenameBase, type);
        String filePath = StringUtils.substringBeforeLast(filename, "/");
        try {
            FileUtils.mkdir(new File(filePath), true);
        } catch (IOException e) {
            logger.warn("Something went wrong while creating screenshot for file {} :", filePath + filename);
        }
        Command command = new Command("PrintText", "PrintText html modi " + filename);
        Result result = connection.doCommand("PrintText html modi " + filename, command);
        if (result.getResultOfCommand().equals("ok")) {
            logger.trace("Wrote screenshot to file='{}'.", filename);
        } else {
            logger.warn("An Error occured while taking screenshots. Could not write screenshot to file='{}'.",
                    filePath + filename);
        }
    }

    @Override
    public void initWithReporter(TestRunReporter reporter) {
        reporter.addListener(this);
    }

    private String getCurrentTestCase() {
        return runningTest != null ? runningTest : "UNKNOWN_TEST";
    }

    private boolean screenshotShouldBeMade(SemanticUnit unit, Action action, String msg) {
        // configurable through maven build?
        return ((action == Action.LEAVE) || unit == SemanticUnit.TEST) && connection.isConnected();
    }

    @Override
    public void reported(SemanticUnit unit, Action action, String message, String id,
            org.testeditor.fixture.core.TestRunReporter.Status status, Map<String, String> variables) {
        if (unit == SemanticUnit.TEST && action == Action.ENTER) {
            runningTest = message;
        }
        if (screenshotShouldBeMade(unit, action, message)) {
            takeScreenshot(message + '.' + action.name());
        }
    }

    @Override
    public void reportFixtureExit(FixtureException fixtureException) {
        // do nothing

    }

    @Override
    public void reportExceptionExit(Exception exception) {
        // do nothing

    }

    @Override
    public void reportAssertionExit(AssertionError assertionError) {
        // do nothing

    }

}