/******************************************************************************* * Copyright (c) 2012 Google, Inc. * 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: * Google, Inc. - initial API and implementation *******************************************************************************/ package com.windowtester.codegen; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.swt.SWT; import com.windowtester.codegen.assembly.block.CodeBlock; import com.windowtester.codegen.assembly.unit.ImportUnit; import com.windowtester.codegen.assembly.unit.MethodUnit; import com.windowtester.codegen.assembly.unit.Modifier; import com.windowtester.codegen.util.CodeGenSnippetBuilder; import com.windowtester.internal.runtime.IWidgetIdentifier; import com.windowtester.recorder.event.IUISemanticEvent; import com.windowtester.recorder.event.meta.RecorderAssertionHookAddedEvent; import com.windowtester.recorder.event.user.SemanticComboSelectionEvent; import com.windowtester.recorder.event.user.SemanticDragEvent; import com.windowtester.recorder.event.user.SemanticDropEvent; import com.windowtester.recorder.event.user.SemanticFocusEvent; import com.windowtester.recorder.event.user.SemanticKeyDownEvent; import com.windowtester.recorder.event.user.SemanticListSelectionEvent; import com.windowtester.recorder.event.user.SemanticMenuSelectionEvent; import com.windowtester.recorder.event.user.SemanticMoveEvent; import com.windowtester.recorder.event.user.SemanticResizeEvent; import com.windowtester.recorder.event.user.SemanticShellClosingEvent; import com.windowtester.recorder.event.user.SemanticShellDisposedEvent; import com.windowtester.recorder.event.user.SemanticShellShowingEvent; import com.windowtester.recorder.event.user.SemanticTableSelectionEvent; import com.windowtester.recorder.event.user.SemanticTreeItemSelectionEvent; import com.windowtester.recorder.event.user.SemanticWidgetClosedEvent; import com.windowtester.recorder.event.user.SemanticWidgetInspectionEvent; import com.windowtester.recorder.event.user.SemanticWidgetSelectionEvent; import com.windowtester.runtime.swt.internal.debug.LogHandler; import com.windowtester.runtime.swt.internal.preferences.ICodeGenConstants; import com.windowtester.swt.WidgetMapper; /** * A code generator that generates test code using the WindowTester test API. * */ public class CodeGenerator extends TestCaseGenerator { /** The name of the instance variable that points to a UIContext */ private String _uiContextInstanceName = "_uiContext"; /** A code snippet builder */ private final CodeGenSnippetBuilder _snippetHelper; /** A mapper instance */ private WidgetMapper _mapper; /** The set of open shells, used to infer needed "wait for" conditions */ //private Set /*<ParentShellInfo>*/ _openShells = new HashSet(); private ShellSet _openShells = new ShellSet(); /** * Create an instance. * @param builder - the builder strategy */ public CodeGenerator(ITestCaseBuilder builder) { super(builder); _mapper = builder.getMapper(); _uiContextInstanceName = builder.getUIContextInstanceName(); _snippetHelper = new CodeGenSnippetBuilder(_uiContextInstanceName); } //////////////////////////////////////////////////////////////////////////// // // Event Handlers // //////////////////////////////////////////////////////////////////////////// /** * @see com.windowtester.codegen.TestCaseGenerator#handleShellShowing(com.windowtester.recorder.event.user.SemanticShellShowingEvent) */ protected void handleShellShowing(SemanticShellShowingEvent event) { /** * If the shell was not already showing we need to generate a wait for condition * TODO: this is abbot's scheme (to wait for Shell by name) but it is not robust (suppose we have 2 shells with the same name?) */ //WidgetLocator info = event.getHierarchyInfo(); //add and check to see if already present... boolean alreadyOpen = !_openShells.add(event); if (!alreadyOpen) addWaitForShell(event.getName()); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleShellDisposed(com.windowtester.recorder.event.user.SemanticShellDisposedEvent) */ protected void handleShellDisposed(SemanticShellDisposedEvent event) { //remove the shell from the open list _openShells.remove(event); addWaitForShellDisposed(event.getName()); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleShellClosing(com.windowtester.recorder.event.user.SemanticShellClosingEvent) */ protected void handleShellClosing(SemanticShellClosingEvent event) { //remove the shell from the open list _openShells.remove(event); //gen closing code: String label = registerWidget(event.getHierarchyInfo()); StringBuffer sb = new StringBuffer(); sb.append(_snippetHelper.closeShellSnippet(label)); CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } private String registerWidget(IWidgetIdentifier locator) { if (locator instanceof com.windowtester.swt.WidgetLocator) return registerWidget((com.windowtester.swt.WidgetLocator)locator); //TODO handle mapper for Swing case return ""; } private String registerWidget(com.windowtester.swt.WidgetLocator locator) { return _mapper.register(locator); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleMove(com.windowtester.recorder.event.user.SemanticMoveEvent) */ protected void handleMove(SemanticMoveEvent event) { StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("move")); } else { String label = registerWidget(info); sb.append(_snippetHelper.moveSnippet(label, event.getX(), event.getY())); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleResize(com.windowtester.recorder.event.user.SemanticResizeEvent) */ protected void handleResize(SemanticResizeEvent event) { StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("resize")); } else { String label = registerWidget(info); sb.append(_snippetHelper.resizeSnippet(label, event.getWidth(), event.getHeight())); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleFocus(com.windowtester.recorder.event.user.SemanticFocusEvent) */ protected void handleFocus(List events) { SemanticFocusEvent curr; SemanticFocusEvent last = null; for (Iterator iter = events.iterator(); iter.hasNext();) { curr = (SemanticFocusEvent)iter.next(); if (last == null || last != null && last.getHierarchyInfo().equals(curr.getHierarchyInfo())) { StringBuffer sb = new StringBuffer(); String label = registerWidget(curr.getHierarchyInfo()); sb.append(_snippetHelper.setFocus(label)); CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); last = curr; } // else, ignore duplicates } } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleButtonClick(com.windowtester.swt.event.model.SemanticWidgetSelectionEvent) */ protected void handleButtonClick(SemanticWidgetSelectionEvent event) { Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "handling button click: " + event.getItemLabel()); //handleShellChange(event); <-- now handled by shell open event StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("click")); } else { String label = registerWidget(info); sb.append(_snippetHelper.clickButtonSnippet(label)); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleMenuInvocation(java.util.List, com.windowtester.swt.event.model.SemanticWidgetSelectionEvent) */ protected void handleMenuInvocation(List events, SemanticMenuSelectionEvent event) { Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "handling menu invocation: "); StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("menu invocation")); } else { String label = registerWidget(event.getHierarchyInfo()); sb.append(_snippetHelper.invokeMenuItemSnippet(label, event.getIndex(), event.getPathString(), event.getButton())); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); //TODO: fix bug in shell change detection --- issue: modal dialog creates new shell (noticed) but it's dismissal is not... //handleShellChange(event); } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleTreeItemSelection(com.windowtester.swt.event.model.SemanticTreeItemSelectionEvent) */ protected void handleTreeItemSelection(SemanticTreeItemSelectionEvent event) { Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "handling tree item selection: " + event.getItemLabel()); StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("tree item selection")); } else { String label = registerWidget(info); sb.append(_snippetHelper.treeItemSelectSnippet(label, event)); } //add import to resolve SWT.CHECK if (event.getChecked()) getTestBuilder().addImport(new ImportUnit("org.eclipse.swt.SWT")); CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleTableItemSelection(com.windowtester.recorder.event.user.SemanticTableSelectionEvent) */ protected void handleTableItemSelection(SemanticTableSelectionEvent event) { // should not be called (table items are just generic selections in SWT) getTestBuilder().add(new CodeBlock("//INTERNAL ERROR: Unexpected Table Item Selection Event")); } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleGenericWidgetSelection(com.windowtester.swt.event.model.SemanticWidgetSelectionEvent) */ protected void handleGenericWidgetSelection(SemanticWidgetSelectionEvent event) { Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "handling generic item selection: " + event.toString()); String code = null; IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { code = createIdErrorMessage("widget selection").toString(); } else { String label = registerWidget(info); //handle import if necessary: String mask = event.getMask(); if (mask != null) getTestBuilder().addImport(new ImportUnit("org.eclipse.swt.SWT")); code = (event.requiresLocationInfo()) ? _snippetHelper.genericWidgetSelection(label, event.getIndex(), event.getMask(), event.getClicks(), event.getX(), event.getY()) : _snippetHelper.genericWidgetSelection(label, event.getIndex(), mask, event.getClicks()); } CodeBlock block = new CodeBlock(code); getTestBuilder().add(block); } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleTextEntry(java.util.List) */ protected void handleTextEntry(List events) { /* * Identification errors are silently ignored here... a warngin message should be produced */ SemanticKeyDownEvent kde = (SemanticKeyDownEvent)events.get(0); //!pq: tentatively removed //handleShellChange(kde); String key = null; StringBuffer sb = new StringBuffer(); String controlKey = null; //used for control character handling IWidgetIdentifier textEntryTarget = null; for (Iterator iter = events.iterator(); iter.hasNext();) { kde = (SemanticKeyDownEvent)iter.next(); key = kde.getKey(); controlKey = null; //reset the controlKey //check for focus change textEntryTarget = kde.getHierarchyInfo(); if (textEntryTarget != null && !textEntryTarget.equals(currentWidgetInfo)) { //update cache... currentWidgetInfo = textEntryTarget; //flush the previous keys if (sb.length() > 0) { String codeSnippet = _snippetHelper.enterTextSnippet(sb.toString()); CodeBlock block = new CodeBlock(codeSnippet); getTestBuilder().add(block); sb = new StringBuffer(); } //handle focus change //!pq: tentatively removing (trusting traverse events to properly ensure focus) //handleFocusChange(textEntryTarget); } //handle "control" keys specially (TABs, LFs) if (isControl(key)) { if (isBackSpace(key)) { /** * If the backspace "mid-stream", it is considered a typo and is corrected for, * otherwise it is emmtited. */ if (sb.length() > 0) //in midstream handleBackspace(sb); else { controlKey = "SWT.BS"; } } controlKey = parseControlKey(kde); //if it's a control char, handle it specially: if (controlKey != null) { //flush the previous keys if (sb.length() > 0) { String codeSnippet = _snippetHelper.enterTextSnippet(sb.toString()); CodeBlock block = new CodeBlock(codeSnippet); getTestBuilder().add(block); sb = new StringBuffer(); } //add import to resolve control key getTestBuilder().addImport(new ImportUnit("org.eclipse.swt.SWT")); //create a control entry snippet String codeSnippet = _snippetHelper.enterKeySnippet(controlKey); CodeBlock block = new CodeBlock(codeSnippet); getTestBuilder().add(block); } //control sequences (ctrl-A) are also special... } else if (kde.isControlSequence()) { //add import to resolve control key getTestBuilder().addImport(new ImportUnit("org.eclipse.swt.SWT")); //create a control entry snippet String codeSnippet = _snippetHelper.enterKeySnippet("SWT.CTRL", key); CodeBlock block = new CodeBlock(codeSnippet); getTestBuilder().add(block); } else { //non-control keys are just appended sb.append(key); } } //flush cached keys (if any) if (sb.length() > 0) { String codeSnippet = _snippetHelper.enterTextSnippet(sb.toString()); CodeBlock block = new CodeBlock(codeSnippet); getTestBuilder().add(block); } } private String parseControlKey(SemanticKeyDownEvent kde) { String key = kde.getKey(); if (isTab(key)) return "SWT.TAB"; if (isEnter(key)) return "SWT.CR"; switch(kde.getKeyCode()) { case SWT.ARROW_RIGHT : return "SWT.ARROW_RIGHT"; case SWT.ARROW_LEFT : return "SWT.ARROW_LEFT"; case SWT.ARROW_UP : return "SWT.ARROW_UP"; case SWT.ARROW_DOWN : return "SWT.ARROW_DOWN"; default : return null; } } /** * Set the ui focus to a new target. * @param newTarget - the new focus target */ protected void handleFocusChange(IWidgetIdentifier newTarget) { StringBuffer sb = new StringBuffer(); //check for error if (newTarget == null) { sb.append(createIdErrorMessage("focus change")); } else { String label = registerWidget(newTarget); sb.append(_snippetHelper.setFocus(label)); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleListSelection(com.windowtester.recorder.event.user.SemanticListSelectionEvent) */ protected void handleListSelection(SemanticListSelectionEvent listSelection) { StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = listSelection.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("list selection")); } else { String label = registerWidget(listSelection.getHierarchyInfo()); String item = listSelection.getItem(); String mask = listSelection.getMask(); if (mask != null) { //add SWT import to identify mask getTestBuilder().addImport(new ImportUnit("org.eclipse.swt.SWT")); } int numClicks = listSelection.getClicks(); sb.append(_snippetHelper.listSelection(label, item, mask, numClicks)); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleComboSelection(com.windowtester.recorder.event.user.SemanticComboSelectionEvent) */ protected void handleComboSelection(SemanticComboSelectionEvent comboSelection) { StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = comboSelection.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("combo selection")); } else { String label = registerWidget(comboSelection.getHierarchyInfo()); String item = comboSelection.getSelection(); sb.append(_snippetHelper.comboSelection(label, item)); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } /** * @see com.windowtester.codegen.TestCaseGenerator#handleDragDrop(com.windowtester.recorder.event.user.SemanticDragEvent, com.windowtester.recorder.event.user.SemanticDropEvent) */ protected void handleDragDrop(SemanticDragEvent drag, SemanticDropEvent drop) { /* * The current implementation is not as clean as it might be. * In particular, thre is a selection event that may not be * necessary (an extra tree selection for instance). To improve * this, we might keep a back point to the last handled event * or current focus and check to see if we really need to update... */ /* * 1) handle hover */ IUISemanticEvent src = drag.getDragSourceEvent(); //first check for path-based selections: if (src instanceof SemanticTreeItemSelectionEvent) { handleTreeItemSelection((SemanticTreeItemSelectionEvent)src); //NOTE: there might be more here: e.g., tables but tables are //treated as widget selections for now } else { handleMouseMoveTo(src); } /* * 2) handle drag */ IUISemanticEvent dest = drop.getDropTargetEvent(); handleDragTo(dest); } protected void handleDrop(SemanticDropEvent event) { //not implemented here } private void handleDragTo(IUISemanticEvent event) { StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); // check for error if (info == null) { sb.append(createIdErrorMessage("dragTo")); } else { String label = registerWidget(info); /* * Handle path here */ String path = null; if (event instanceof SemanticTreeItemSelectionEvent) { path = ((SemanticTreeItemSelectionEvent)event).getPathString(); } sb.append(_snippetHelper.dragToSnippet(label, path, event.getX(), event.getY())); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } private void handleMouseMoveTo(IUISemanticEvent event) { StringBuffer sb = new StringBuffer(); IWidgetIdentifier info = event.getHierarchyInfo(); //check for error if (info == null) { sb.append(createIdErrorMessage("mouseMoveTo")); } else { String label = registerWidget(info); sb.append(_snippetHelper.mouseMoveToSnippet(label, event.getX(), event.getY())); } CodeBlock block = new CodeBlock(sb.toString()); getTestBuilder().add(block); } protected void handleAssertion(RecorderAssertionHookAddedEvent assertEvent) { String method = getTestBuilder().getFreshMethod(assertEvent.getHookName()); MethodUnit assertMethod = new MethodUnit(method,"// TODO Auto-generated method stub" + NEW_LINE); assertMethod.addModifier(Modifier.PROTECTED); assertMethod.addThrows("Exception"); CodeBlock block = new CodeBlock(_snippetHelper.methodInvocation(method)); getTestBuilder().add(block); getTestBuilder().addMethod(assertMethod); } protected void handleInspection(SemanticWidgetInspectionEvent event) { //no op in legacy API } /* (non-Javadoc) * @see com.windowtester.codegen.TestCaseGenerator#handleWidgetClosed(com.windowtester.recorder.event.user.SemanticWidgetClosedEvent) */ protected void handleWidgetClosed(SemanticWidgetClosedEvent event) { //no op in legacy API } //////////////////////////////////////////////////////////////////////////// // // Helpers // //////////////////////////////////////////////////////////////////////////// protected StringBuffer createIdErrorMessage(String eventType, String comment) { StringBuffer sb = new StringBuffer(); sb.append("//error identifying target of ").append(eventType).append(" event;").append(comment).append(NEW_LINE); return sb; } protected StringBuffer createIdErrorMessage(String eventType) { return createIdErrorMessage(eventType, "event ignored"); //a default comment } /** * Check for control characters * @param key - the character to check * @return true if the key is a control character */ private boolean isControl(String key) { return key != null && Character.isISOControl(key.charAt(0)); } /** * Check for a backspace. * @param key - the character to check * @return true if the key is a backspace */ private boolean isBackSpace(String key) { return key.charAt(0) == '\b'; } /** * Check for a tab. * @param key - the character to check * @return true if the key is a tab */ private boolean isTab(String key) { return key.charAt(0) == '\t'; } /** * Check for an enter key. * @param key - the character to check * @return true if the key is an 'enter' */ private boolean isEnter(String key) { return key.charAt(0) == ICodeGenConstants.NEW_LINE.charAt(0) || key.charAt(0) == '\r'; } /** * Handle a backspace by deleting a char from the end of the buffer * (if there is a char to delete). * @param sb - the string buffer */ private void handleBackspace(StringBuffer sb) { //System.out.print("<backspace>"); int last = sb.length() - 1; if (last >= 0) sb.deleteCharAt(last); } // /** // * Handle when a shell change event occurs by injecting the appropriate wait condition // * @param event - the UI event // */ // private void handleShellChange(IUISemanticEvent event) { // String shellTitle = event.getParentShellTitle(); // if (_builder.updateCurrentShellTitle(shellTitle)) { // if (shellTitle != null) { // addWaitForShell(shellTitle); // } // } // } /** * Handle when a shell change event occurs by injecting the appropriate wait condition * @param shellTitle - the shell title */ private void addWaitForShell(String shellTitle) { Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "(requires a wait for: " + shellTitle +")"); String waitBody = _uiContextInstanceName + ".waitForShellShowing(\"" + shellTitle + "\");" + NEW_LINE; CodeBlock wait = new CodeBlock(waitBody); getTestBuilder().add(wait); } /** * Handle when a shell dispose event occurs by injecting the appropriate wait condition * @param shellTitle - the shell title */ private void addWaitForShellDisposed(String shellTitle) { if (shellTitle == null || shellTitle.equals("") || shellTitle.length() ==0) { //empty shell titles are ignored for now (these are usually component frames) Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "wait for empty shell title ignored"); return; } Tracer.trace(ICodeGenPluginTraceOptions.CODEGEN, "(requires a wait for dispose: " + shellTitle +")"); String waitBody = _uiContextInstanceName + ".waitForShellDisposed(\"" + shellTitle + "\");" + NEW_LINE; CodeBlock wait = new CodeBlock(waitBody); getTestBuilder().add(wait); } protected static final class ShellSet { Set _shellTitles = new HashSet(); boolean add(String title) { return _shellTitles.add(title); } boolean remove(String title) { return _shellTitles.remove(title); } boolean add(SemanticShellShowingEvent event) { String title = event.getName(); if (title == null) { LogHandler.log("Shell title null --- ignoring in add"); return false; } return _shellTitles.add(title); } boolean remove(SemanticShellClosingEvent event) { String title = event.getName(); if (title == null) { LogHandler.log("Shell title null --- ignoring in remove"); return false; } return _shellTitles.remove(title); } boolean remove(SemanticShellDisposedEvent event) { String title = event.getName(); if (title == null) { LogHandler.log("Shell title null --- ignoring in remove"); return false; } return _shellTitles.remove(title); } boolean contains(String title) { return _shellTitles.contains(title); } } }