Search code examples
javaeclipsejuniteclemmatestfx

EclEmma "Coverage As" Eclipse JUnit Thread Death


I have the below JUnit4 test written using TestFX to test the GUI (JavaFX) of a particular product (a to-do list), along with two necessary classes. The first class is the main class which manages the whole GUI, while the second is the class for the text field. The complete source code is located here if necessary (it was part of a school project that is already submitted).

The test works perfectly fine if I just run it with the F11 hotkey in Eclipse, or "Run As -> JUnit Test". However, when I select "Coverage", it bugs on the very first test case (regardless of which I choose to set as first). Specifically, it "types" the first two characters of the first test case (sh in the sample case here), and then gives me the error that user input was detected ([TestFX] User mouse movement detected. Aborting test.) It then moves onto the next test case.

I have been unable to figure it out on my own, and I can't seem to find much help online about this. Any assistance would be greatly appreciated! Based on the stack trace it looks to be something about the thread, but I can't see how the coverage running will cause that (when normal testing doesn't).

I had to cut the stack trace short because I hit the limit.

java.lang.RuntimeException: java.lang.ThreadDeath
at org.loadui.testfx.utils.FXTestUtils.awaitEvents(FXTestUtils.java:104)
at org.loadui.testfx.FXScreenController.release(FXScreenController.java:131)
at org.loadui.testfx.GuiTest.release(GuiTest.java:1110)
at org.loadui.testfx.GuiTest.type(GuiTest.java:1069)
at org.loadui.testfx.GuiTest.type(GuiTest.java:1008)
at org.loadui.testfx.GuiTest.type(GuiTest.java:990)
at gui.UserInterfaceTest.test1ShowUndoneEmpty(UserInterfaceTest.java:38)
Caused by: java.lang.ThreadDeath
at java.lang.Thread.stop(Unknown Source)
at org.loadui.testfx.utils.UserInputDetector.userInputDetected(UserInputDetector.java:58)
at org.loadui.testfx.utils.UserInputDetector.assertPointsAreEqual(UserInputDetector.java:42)
at org.loadui.testfx.utils.UserInputDetector.run(UserInputDetector.java:27)
at java.lang.Thread.run(Unknown Source)

UserInterface.java

package gui;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.scene.Scene;
import object.Task;
import type.CommandType;
import type.KeywordType;
import logic.FeedbackHelper;
import logic.LogicController;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;

//@@author A0112882H
public class UserInterface extends Application {
    private static final int ROW_HEIGHT = 30;

    private static BorderPane _root = new BorderPane();
    private static Scene _defaultScene = new Scene(_root, 750, 580);
    private static VBox _vbox = new VBox();
    private static VBox _tables = new VBox();

    private static UIButton _taskButton = new UIButton("Tasks & Events");
    private static UIButton _floatingButton = new UIButton("Floating Tasks");
    private static UITextField _field = new UITextField();

    private static TextArea _cheatSheet = new TextArea();
    private static Label _feedBack = new Label();
    private static int commandIndex;

    private static UITable _taskTable = new UITable(false);
    private static UITable _floatingTable = new UITable(true);

    private final KeyCombination _undoKey = new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN);
    private final KeyCombination _redoKey = new KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN);
    private final KeyCombination _homeKey = new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN);

    private static ArrayList<String> commandHistory = new ArrayList<String>();
    private static ArrayList<Task> _displayList = new ArrayList<Task>();

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        _root.setOnKeyPressed(hotKeyEvents);
        _field.setOnKeyPressed(hotKeyEvents);

        setScene();
        setUpCommandPrompt();
        setUpTables();

        setKeywordsHighlighting();

        primaryStage.setScene(_defaultScene);
        primaryStage.setTitle("F2DO");
        primaryStage.show();
    }

    public BorderPane getRootNode() {
        return _root;
    }

    private void setScene() {
        String css = UserInterface.class.getResource("style.css").toExternalForm();
        _defaultScene.getStylesheets().add(css);

        _defaultScene.heightProperty().addListener(new ChangeListener<Number>() {

            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                int displaySize = (int) Math.floor(_taskTable.getHeight() / ROW_HEIGHT) - 1;
                LogicController.setNonFloatingDisplaySize(displaySize);
                updateDisplayList();
            }

        });
    }

    /**
     * Set the hot keys. Ctrl + U: undo operation. Ctrl + R: redo operation.
     * Ctrl + H: home page. F1: help page. F2: show all. F3: show undone tasks.
     * F4: show done tasks. ESC: exit application.
     */
    private EventHandler<KeyEvent> hotKeyEvents = new EventHandler<KeyEvent>() {

        @Override
        public void handle(KeyEvent event) {

            String showUndone = "show undone";
            String showDone = "show done";
            String showAll = "show all";

            if (_undoKey.match(event)) {
                String feedbackMsg = LogicController.undo();
                _feedBack.setText(feedbackMsg);
                updateDisplayList();
            } else if (_redoKey.match(event)) {
                String feedbackMsg = LogicController.redo();
                _feedBack.setText(feedbackMsg);
                updateDisplayList();
            } else if (_homeKey.match(event)) {
                initialiseScene();
                setUpCommandPrompt();
                setUpTables();
            } else if (event.getCode().equals(KeyCode.F3)) {
                String feedbackMsg = LogicController.process(showUndone, _displayList);
                _feedBack.setText(feedbackMsg);
                updateDisplayList();
            } else if (event.getCode().equals(KeyCode.F4)) {
                String feedbackMsg = LogicController.process(showDone, _displayList);
                _feedBack.setText(feedbackMsg);
                updateDisplayList();
            } else if (event.getCode().equals(KeyCode.F2)) {
                String feedbackMsg = LogicController.process(showAll, _displayList);
                _feedBack.setText(feedbackMsg);
                updateDisplayList();
            } else if (event.getCode().equals(KeyCode.F1)) {
                try {
                    initialiseScene();
                    setUpCommandPrompt();
                    setCheatSheetContent();
                } catch (Exception e) {
                }
            } else if (event.getCode().equals(KeyCode.ESCAPE)) {
                exit();
            } else if (event.getCode().equals(KeyCode.ENTER)) {
                String userInput = _field.getText();
                commandHistory.add(userInput);
                commandIndex = commandHistory.size() - 1;

                _field.clear();
                event.consume();

                String feedbackMsg = LogicController.process(userInput, _displayList);

                if (feedbackMsg == FeedbackHelper.MSG_HELP) {
                    try {
                        initialiseScene();
                        setUpCommandPrompt();
                        setCheatSheetContent();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else if (feedbackMsg == FeedbackHelper.MSG_HOME) {
                    initialiseScene();
                    setUpCommandPrompt();
                    setUpTables();
                } else {
                    _feedBack.setText(feedbackMsg);
                    updateDisplayList();
                }
            } else if (event.getCode().equals(KeyCode.UP)) {

                if (!commandHistory.isEmpty()) {
                    _field.replaceText(commandHistory.get(commandIndex));
                    int length = commandHistory.get(commandIndex).length();
                    commandIndex--;

                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            _field.positionCaret(length);
                        }
                    });

                    if (commandIndex < 0) {
                        commandIndex = 0;
                    }
                }
            } else if (event.getCode().equals(KeyCode.DOWN)) {
                _field.showPopup();
            }

        }

    };

    /**
     * Set up command prompt and feedback
     */
    private void setUpCommandPrompt() {
        setTextArea();
        setFeedback();

        _field.setId("textarea");
        _feedBack.setId("feedback");

        _vbox.setAlignment(Pos.CENTER);
        _vbox.setSpacing(5);
        _vbox.getChildren().addAll(_field, _feedBack);
        BorderPane.setMargin(_vbox, new Insets(20, 20, 0, 20));

        _root.setTop(_vbox);
    }

    /**
     * Set up labels and tables
     */
    private void setUpTables() {
        updateDisplayList();

        BorderPane.setMargin(_tables, new Insets(8, 20, 30, 20));
        BorderPane.setAlignment(_tables, Pos.CENTER);

        _floatingTable.setId("floatingTable");
        _taskTable.setId("taskTable");

        _taskButton.setMaxWidth(Double.MAX_VALUE);
        _floatingButton.setMaxWidth(Double.MAX_VALUE);
        _taskButton.setStyle("-fx-font-size: 13.5; -fx-font-weight: bold");
        _floatingButton.setStyle("-fx-font-size: 13.5; -fx-font-weight: bold");

        _tables.setAlignment(Pos.CENTER);
        _tables.getChildren().addAll(_taskButton, _taskTable, _floatingButton, _floatingTable);
        _tables.setSpacing(7);

        _root.setCenter(_tables);
    }

    /**
     * Update tables.
     */
    private static void updateDisplayList() {
        ArrayList<Task> nonFloatingList = LogicController.getNonFloatingList();
        ArrayList<Task> floatingList = LogicController.getFloatingList();

        _displayList.clear();
        _displayList.addAll(nonFloatingList);
        _displayList.addAll(floatingList);

        _taskTable.updateTable(nonFloatingList, floatingList);
        _floatingTable.updateTable(nonFloatingList, floatingList);
        _field.updateDisplayList(_displayList);
    }

    /**
     * Set the design of textArea
     */
    private void setTextArea() {
        _field.setPrefHeight(25);
        _field.setMaxHeight(25);
        _field.setPadding(new Insets(2, 2, 2, 2));
        _field.setWrapText(true);
        _field.setStyle("-fx-border-color: lightblue; -fx-font-size: 14");
    }

    /**
     * Set the design of feedback.
     * 
     * @param feedback
     */
    private void setFeedback() {
        _feedBack.setText("Welcome to F2DO, your personalised task manager(:\n" + "Type " + "\"Help\""
                + " for a list of commands to get started.");
        _feedBack.setMouseTransparent(true);
    }

    /**
     * Set highlighting of the keyword.
     */
    private void setKeywordsHighlighting() {

        _field.textProperty().addListener((observable, oldValue, newValue) -> {
            // check if the first word is a keyword - happens in most cases
            // for commands e.g. like add, search, edit, delete
            String firstWord = getFirstWord(newValue);

            if (isValidCmd(firstWord)) {
                _field.setStyle(0, firstWord.length(), "-fx-font-weight: bold; -fx-fill: red");
                if (newValue.length() > firstWord.length()) {
                    _field.setStyle(firstWord.length() + 1, newValue.length(),
                            "-fx-font-weight: normal; -fx-fill: black");
                }

                String[] result = newValue.substring(firstWord.length()).split("\\s");
                int currentIndex = firstWord.length();
                for (int i = 0; i < result.length; i++) {
                    String word = result[i];
                    if (isValidKeyword(word)) {
                        _field.setStyle(currentIndex, currentIndex + word.length(),
                                "-fx-font-weight: bold; -fx-fill: blue");
                    }
                    currentIndex += word.length() + 1;
                }

            } else {
                _field.setStyle(0, newValue.length(), "-fx-font-weight: normal; -fx-fill: black");
            }
        });
    }

    /**
     * Get the first word of the command.
     * 
     * @param newCommand
     *            - input command
     * @return first word
     */
    private String getFirstWord(String newCommand) {

        String[] textTokens = newCommand.split(" ");

        if (textTokens.length > 0) {
            return textTokens[0];
        }

        return null;
    }

    /**
     * Check if the entered word is a valid command.
     * 
     * @param word
     *            - input word
     * @return true if the word is a valid command; false otherwise
     */
    private boolean isValidCmd(String word) {

        if (CommandType.toCmd(word) != CommandType.INVALID) {
            return true;
        }
        return false;
    }

    /**
     * Check if the entered word is a valid keyword.
     * 
     * @param word
     *            - input word
     * @return true if the word is a valid keyword; false otherwise
     */
    private boolean isValidKeyword(String word) {

        if (KeywordType.toType(word) != KeywordType.INVALID) {
            return true;
        }
        return false;
    }

    private void initialiseScene() {
        _vbox.getChildren().clear();
        _tables.getChildren().clear();
        _root.getChildren().clear();
    }

    private void setCheatSheetContent() throws IOException {
        String text;
        StringBuilder content = new StringBuilder();

        _cheatSheet.setEditable(false);

        BorderPane.setMargin(_cheatSheet, new Insets(8, 20, 25, 20));

        InputStream is = getClass().getResourceAsStream("cheatsheet.txt");
        BufferedReader br = new BufferedReader(new InputStreamReader(is));

        while ((text = br.readLine()) != null) {
            content.append(text).append("\n");
        }

        _cheatSheet.clear();
        _cheatSheet.appendText(content.toString());

        _root.setCenter(_cheatSheet);

        br.close();
    }

    private void exit() {
        Platform.exit();
    }
}

UITextField.java

package gui;

import org.fxmisc.richtext.InlineCssTextArea;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import object.Task;
import type.CommandType;
import type.TaskType;

//@@author A0118005W
public class UITextField extends InlineCssTextArea {
    private static ArrayList<Task> _displayList = new ArrayList<Task>();
    private ContextMenu popupMenu = new ContextMenu();

    public UITextField() {
        super();
        setAutoFill();
    }

    /**
     * Update the display list in TextField.
     * 
     * @param displayList
     *            - display list
     */
    public void updateDisplayList(ArrayList<Task> displayList) {
        _displayList = displayList;
    }

    /**
     * Show pop-up menu.
     */
    public void showPopup() {
        if (!popupMenu.isShowing()) {
            popupMenu.show(this, Side.BOTTOM, 0, 0);
        }
    }

    /**
     * Set up auto fill in of the text field.
     */
    private void setAutoFill() {
        this.textProperty().addListener(new ChangeListener<String>() {

            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                String text = UITextField.this.getText();
                String[] textTokens = text.split(" ");

                popupMenu.hide();

                int spaceCount = 0;
                for (int i = 0; i < text.length() && spaceCount < 2; i++) {
                    if (text.charAt(i) == ' ') {
                        spaceCount += 1;
                    }
                }

                if (textTokens.length == 2 && spaceCount == 2) {
                    String firstToken = textTokens[0];
                    CommandType cmd = CommandType.toCmd(firstToken);
                    int index = getInteger(textTokens[1]) - 1;
                    boolean isWithinRange = ((index >= 0) && (index < _displayList.size()));

                    if (cmd == CommandType.EDIT && isWithinRange) {
                        Task task = _displayList.get(index);
                        populatePopup(index, task);

                        if (!popupMenu.isShowing()) {
                            popupMenu.show(UITextField.this, Side.BOTTOM, 0, 0);
                        }
                    }
                } else if (textTokens.length <= 2) {
                    // Hide pop up
                    popupMenu.hide();
                    popupMenu.getItems().clear();
                }

            }

        });
    }

    /**
     * Get the integer from an input string. If the input cannot be parsed,
     * return -1.
     * 
     * @param input
     *            - input string
     * @return parsed integer
     */
    private int getInteger(String input) {
        try {
            int integer = Integer.parseInt(input);
            return integer;
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    /**
     * Populate the pop-up box.
     * 
     * @param index
     *            - index of the task
     * @param task
     *            - task to be displayed
     */
    private void populatePopup(int index, Task task) {
        ArrayList<String> displayList = getDisplayItems(index, task);
        ArrayList<CustomMenuItem> menuItems = new ArrayList<CustomMenuItem>();

        for (int i = 0; i < displayList.size(); i++) {
            String str = displayList.get(i);
            Label label = new Label(str);
            CustomMenuItem item = new CustomMenuItem(label, true);

            item.setOnAction(new EventHandler<ActionEvent>() {

                @Override
                public void handle(ActionEvent event) {
                    replaceText(str);
                    positionCaret(str.length());
                }

            });

            menuItems.add(item);
        }

        popupMenu.getItems().clear();
        popupMenu.getItems().addAll(menuItems);
    }

    /**
     * Get the command input to be displayed in the pop-up menu.
     * 
     * @param index
     *            - index of the task
     * @param task
     *            - task to be displayed
     * @return display items
     */
    private ArrayList<String> getDisplayItems(int index, Task task) {
        ArrayList<String> items = new ArrayList<String>();
        TaskType taskType = task.getTaskType();

        Integer displayIndex = index + 1;
        String floatingStr = "edit " + displayIndex.toString() + " " + task.getTaskName() + " ";
        String eventStr = floatingStr;
        String alternateEventStr = floatingStr;
        String deadlineStr = floatingStr;

        Calendar tmrCalendar = Calendar.getInstance();
        Calendar afterTmrCalendar = Calendar.getInstance();
        tmrCalendar.add(Calendar.DAY_OF_MONTH, 1);
        afterTmrCalendar.add(Calendar.DAY_OF_MONTH, 2);

        Date tomorrow = tmrCalendar.getTime();
        Date afterTomorrow = afterTmrCalendar.getTime();
        Date startDate = task.getStartDate();
        Date endDate = task.getEndDate();

        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm");

        // Set event string
        if (startDate != null && endDate != null) {
            eventStr += "from " + dateFormat.format(startDate) + " ";
            eventStr += "to " + dateFormat.format(endDate);

            alternateEventStr += "on " + dateFormat.format(startDate);
        } else if (startDate != null) {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(startDate);
            calendar.add(Calendar.DAY_OF_MONTH, 1);

            eventStr += "on " + dateFormat.format(startDate);

            alternateEventStr += "from " + dateFormat.format(startDate) + " ";
            alternateEventStr += "to " + dateFormat.format(calendar.getTime());
        } else if (endDate != null) {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(endDate);
            calendar.add(Calendar.DAY_OF_MONTH, 1);

            eventStr += "from " + dateFormat.format(endDate) + " ";
            eventStr += "to " + dateFormat.format(calendar.getTime());

            alternateEventStr += "on " + dateFormat.format(endDate);

        } else {
            eventStr += "from " + dateFormat.format(tomorrow) + " ";
            eventStr += "to " + dateFormat.format(afterTomorrow);

            alternateEventStr += "on " + dateFormat.format(tomorrow);
        }

        // Set deadline string
        if (endDate != null) {
            deadlineStr += "by " + dateFormat.format(endDate);
        } else if (startDate != null) {
            deadlineStr += "by " + dateFormat.format(startDate);
        } else {
            deadlineStr += "by " + dateFormat.format(tomorrow);
        }

        // Assign display order
        int eventIndex = 0;
        int floatingIndex = 1;
        int alternateEventIndex = 2;
        int deadlineIndex = 3;
        int firstIndex = -1;

        String[] eventList = { eventStr, floatingStr, alternateEventStr, deadlineStr };

        switch (taskType) {
        case EVENT:
            if (endDate == null) {
                items.add(eventList[alternateEventIndex]);
                firstIndex = alternateEventIndex;
            } else {
                items.add(eventList[eventIndex]);
                firstIndex = eventIndex;
            }
            break;
        case DEADLINE:
            items.add(eventList[deadlineIndex]);
            firstIndex = deadlineIndex;
            break;
        case FLOATING:
            items.add(eventList[floatingIndex]);
            firstIndex = floatingIndex;
            break;
        default:
            // Do nothing
        }

        for (int i = 0; i < eventList.length; i++) {
            if (i != firstIndex) {
                items.add(eventList[i]);
            }
        }

        return items;
    }
}

UserInterfaceTest.java

package gui;

import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
import org.junit.Test;
import org.loadui.testfx.Assertions;
import org.loadui.testfx.GuiTest;
import org.loadui.testfx.utils.FXTestUtils;

import javafx.scene.Parent;
import javafx.scene.input.KeyCode;

//@@author A0112882H-reused
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class UserInterfaceTest {
    private static GuiTest controller;

    @BeforeClass
    public static void setUpClass() throws InterruptedException {
        FXTestUtils.launchApp(UserInterface.class);
        Thread.sleep(7000); // Giving the program time to startup. The likely problematic line.

        controller = new GuiTest() {
            @Override
            protected Parent getRootNode() {
                return stage.getScene().getRoot();
            }
        };
        System.out.println("GUI TEST START");

    }

    // @@author A0112882H
    @Test
    public void test1ShowUndoneEmpty() throws Exception {
        UITextField textField = (UITextField) GuiTest.find("#textarea");
        controller.click(textField).type("show undone").push(KeyCode.ENTER);
        // Assertions.assertNodeExists("");
    }

    @Test
    public void test2AddFloatingTask() throws Exception {
        UITextField textField = (UITextField) GuiTest.find("#textarea");
        controller.click(textField).type("add Meeting with boss").push(KeyCode.ENTER);
        Assertions.assertNodeExists("Meeting with boss");
    }

    @Test
    public void test3Search() throws Exception {
        UITextField textField = (UITextField) GuiTest.find("#textarea");
        controller.click(textField).type("search Meeting with boss").push(KeyCode.ENTER);
        Assertions.assertNodeExists("Meeting with boss");
    }

    @Test
    public void test4ShowUndone() throws Exception {
        UITextField textField = (UITextField) GuiTest.find("#textarea");
        controller.click(textField).type("show undone").push(KeyCode.ENTER);
        Assertions.assertNodeExists("Meeting with boss");
    }

    @Test
    public void test5MarkDone() throws Exception {
        UITextField textField = (UITextField) GuiTest.find("#textarea");
        controller.click(textField).type("done 1").push(KeyCode.ENTER);
        // Assertions.assertNodeExists("Meeting with boss");
    }
}

P.S. Sorry if I added unnecessary tags. Unsure as to what I should have included.

P.P.S. I never did get the assertions in the test file to completely work. You may ignore them if you wish to, since I am not looking to learn how to get that resolved (for now).


Solution

  • Your Thread is being interrupted (stopped) from an external source, and the Exception is thrown out, interrupting the test as well. Just catch it and ignore it.

    try {
        Thread.sleep(7000);
    } 
    catch (InterruptedException e) {}
    

    A good test should be fast, and not depends on external events or classes (except the one you are testing). Maybe you should try to make smaller, more targeted tests.

    Since the GUI is working inside its own Thread, the end of your own JUnit test will kill it. Make sure you shutdown the GUI properly.