diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..e0f15db2eb22b5d618150277e48b741f8fdd277a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/javafx-template/src/main/java/app/Calc.java b/javafx-template/src/main/java/app/Calc.java index 8ff70b8fedecadf649989648439d43499a485763..1f0c5c8d14d2fda17574ab3c35e5365e00eb6b74 100644 --- a/javafx-template/src/main/java/app/Calc.java +++ b/javafx-template/src/main/java/app/Calc.java @@ -25,6 +25,7 @@ public class Calc { /** * Pushes a new operand onto top of the stack. + * * @param d the new operand */ public void pushOperand(double d) { @@ -52,6 +53,7 @@ public class Calc { /** * Removes and returns the top operand. + * * @return the top operand * @throws IllegalStateException if the stack is empty */ @@ -65,6 +67,7 @@ public class Calc { /** * Performs the provided operation in the top operand, and * replaces it with the result. + * * @param op the operation to perform * @return the result of performing the operation * @throws IllegalStateException if the operand stack is empty @@ -77,6 +80,7 @@ public class Calc { /** * Performs the provided operation in the two topmost operands, and * replaces them with the result. + * * @param op the operation to perform * @return the result of performing the operation * @throws IllegalStateException if the operand count is less than two @@ -94,6 +98,8 @@ public class Calc { /** * Swaps the two topmost operands. + * + * @throws IllegalStateException if the operand count is less than two */ public void swap() { // TODO @@ -101,6 +107,8 @@ public class Calc { /** * Duplicates the top operand. + * + * @throws IllegalStateException if the operand stack is empty */ public void dup() { // TODO diff --git a/javafx-template/src/test/java/app/CalcTest.java b/javafx-template/src/test/java/app/CalcTest.java index 9a7c0df0cc7517529e1b579ed515ddc89a8e8eda..28064cf8f1d027ac48e3f8f2a377f1251c6d1b24 100644 --- a/javafx-template/src/test/java/app/CalcTest.java +++ b/javafx-template/src/test/java/app/CalcTest.java @@ -28,6 +28,21 @@ public class CalcTest { checkCalc(calc, 3.14, 1.0); } + @Test + public void testPeekOperand() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.peekOperand()); + Assertions.assertThrows(IllegalArgumentException.class, () -> new Calc().peekOperand()); + } + + @Test + public void testPeekOperandN() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.peekOperand(0)); + Assertions.assertEquals(1.0, calc.peekOperand(1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> calc.peekOperand(2)); + } + @Test public void testPopOperand() { Calc calc = new Calc(1.0, 3.14); @@ -36,4 +51,64 @@ public class CalcTest { Assertions.assertEquals(1.0, calc.popOperand()); checkCalc(calc); } + + @Test + public void testPopOperand_emptyStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().popOperand()); + } + + @Test + public void testPerformOperation1() { + Calc calc = new Calc(1.0); + Assertions.assertEquals(-1.0, calc.performOperation(n -> -n)); + checkCalc(calc, -1.0); + } + + @Test + public void testPerformOperation1_emptyOperandStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation(n -> -n)); + } + + + @Test + public void testPerformOperation2() { + Calc calc = new Calc(1.0, 3.0); + Assertions.assertEquals(-2.0, calc.performOperation((n1, n2) -> n1 - n2)); + checkCalc(calc, -2.0); + } + + @Test + public void testPerformOperation2_lessThanTwoOperands() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).performOperation((n1, n2) -> n1 - n2)); + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation((n1, n2) -> n1 - n2)); + } + + @Test + public void testSwap() { + Calc calc = new Calc(1.0, 3.14); + calc.swap(); + checkCalc(calc, 3.14, 1.0); + calc.swap(); + checkCalc(calc, 1.0, 3.14); + } + + @Test + public void testSwap_lessThanTwoOperands() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).swap()); + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().swap()); + } + + @Test + public void testDup() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.popOperand()); + checkCalc(calc, 1.0); + Assertions.assertEquals(1.0, calc.popOperand()); + checkCalc(calc); + } + + @Test + public void testDup_emptyOperandStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().dup()); + } } diff --git a/modules-template/.gitignore b/modules-template/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..213163975bb915006004bbbf5a9c7d75f6598f95 --- /dev/null +++ b/modules-template/.gitignore @@ -0,0 +1,2 @@ +# ignore maven build folder +target/ diff --git a/modules-template/core/pom.xml b/modules-template/core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..abaca3ef9701d5cdeea0f847482f8c86d62b5ca2 --- /dev/null +++ b/modules-template/core/pom.xml @@ -0,0 +1,43 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>it1901</groupId> + <artifactId>modules-core</artifactId> + + <parent> + <groupId>it1901</groupId> + <artifactId>modules-template</artifactId> + <version>0.0.1-SNAPSHOT</version> + <relativePath>..</relativePath> + </parent> + + <dependencies> + + <!-- junit testing with jupiter --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + </plugin> + </plugins> + </build> +</project> diff --git a/modules-template/core/src/main/java/core/Calc.java b/modules-template/core/src/main/java/core/Calc.java new file mode 100644 index 0000000000000000000000000000000000000000..f50679df535792a6c53eb7d044ff443c2b682bd3 --- /dev/null +++ b/modules-template/core/src/main/java/core/Calc.java @@ -0,0 +1,108 @@ +package core; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.UnaryOperator; + +public class Calc { + + private final List<Double> operandStack; + + public Calc(double... operands) { + operandStack = new ArrayList<>(operands.length + 2); + for (var d : operands) { + operandStack.add(d); + } + } + + /** + * @return the number of operands on the stack + */ + public int getOperandCount() { + return operandStack.size(); + } + + /** + * Pushes a new operand onto top of the stack. + * @param d the new operand + */ + public void pushOperand(double d) { + operandStack.add(d); + } + + /** + * @param n the place (from the top) to peek + * @return the n'th operand from the top + * @throws IllegalArgumentException if n is larger than the operand count + */ + public double peekOperand(int n) { + if (n > getOperandCount()) { + throw new IllegalArgumentException("Cannot peek at position " + n + " when the operand count is " + getOperandCount()); + } + return operandStack.get(getOperandCount() - n - 1); + } + + /** + * @return the top operand + */ + public double peekOperand() { + return peekOperand(0); + } + + /** + * Removes and returns the top operand. + * @return the top operand + * @throws IllegalStateException if the stack is empty + */ + public double popOperand() { + if (getOperandCount() == 0) { + throw new IllegalStateException("Cannot pop from an empty stack"); + } + return operandStack.remove(operandStack.size() - 1); + } + + /** + * Performs the provided operation in the top operand, and + * replaces it with the result. + * @param op the operation to perform + * @return the result of performing the operation + * @throws IllegalStateException if the operand stack is empty + */ + public double performOperation(UnaryOperator<Double> op) throws IllegalStateException { + // TODO + return 0.0; + } + + /** + * Performs the provided operation in the two topmost operands, and + * replaces them with the result. + * @param op the operation to perform + * @return the result of performing the operation + * @throws IllegalStateException if the operand count is less than two + */ + public double performOperation(BinaryOperator<Double> op) throws IllegalStateException { + if (getOperandCount() < 2) { + throw new IllegalStateException("Too few operands (" + getOperandCount() + ") on the stack"); + } + var op1 = popOperand(); + var op2 = popOperand(); + var result = op.apply(op1, op2); + pushOperand(result); + return result; + } + + /** + * Swaps the two topmost operands. + */ + public void swap() { + // TODO + } + + /** + * Duplicates the top operand. + */ + public void dup() { + // TODO + } +} \ No newline at end of file diff --git a/modules-template/core/src/test/java/core/CalcTest.java b/modules-template/core/src/test/java/core/CalcTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0207cd5a5d7ed3f9a44bb1e3a159d33c91f8dacb --- /dev/null +++ b/modules-template/core/src/test/java/core/CalcTest.java @@ -0,0 +1,114 @@ +package core; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CalcTest { + + private static void checkCalc(Calc calc, double... operands) { + Assertions.assertEquals(operands.length, calc.getOperandCount(), "Wrong operand count"); + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], calc.peekOperand(i), "Wrong value at #" + i + " of operand stack"); + } + } + + @Test + public void testCalc() { + checkCalc(new Calc()); + checkCalc(new Calc(1.0), 1.0); + checkCalc(new Calc(3.14, 1.0), 1.0, 3.14); + } + + @Test + public void testPushOperand() { + Calc calc = new Calc(); + calc.pushOperand(1.0); + checkCalc(calc, 1.0); + calc.pushOperand(3.14); + checkCalc(calc, 3.14, 1.0); + } + + @Test + public void testPeekOperand() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.peekOperand()); + Assertions.assertThrows(IllegalArgumentException.class, () -> new Calc().peekOperand()); + } + + @Test + public void testPeekOperandN() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.peekOperand(0)); + Assertions.assertEquals(1.0, calc.peekOperand(1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> calc.peekOperand(2)); + } + + @Test + public void testPopOperand() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.popOperand()); + checkCalc(calc, 1.0); + Assertions.assertEquals(1.0, calc.popOperand()); + checkCalc(calc); + } + + @Test + public void testPopOperand_emptyStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().popOperand()); + } + + @Test + public void testPerformOperation1() { + Calc calc = new Calc(1.0); + Assertions.assertEquals(-1.0, calc.performOperation(n -> -n)); + checkCalc(calc, -1.0); + } + + @Test + public void testPerformOperation1_emptyOperandStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation(n -> -n)); + } + + + @Test + public void testPerformOperation2() { + Calc calc = new Calc(1.0, 3.0); + Assertions.assertEquals(-2.0, calc.performOperation((n1, n2) -> n1 - n2)); + checkCalc(calc, -2.0); + } + + @Test + public void testPerformOperation2_lessThanTwoOperands() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).performOperation((n1, n2) -> n1 - n2)); + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation((n1, n2) -> n1 - n2)); + } + + @Test + public void testSwap() { + Calc calc = new Calc(1.0, 3.14); + calc.swap(); + checkCalc(calc, 3.14, 1.0); + calc.swap(); + checkCalc(calc, 1.0, 3.14); + } + + @Test + public void testSwap_lessThanTwoOperands() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).swap()); + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().swap()); + } + + @Test + public void testDup() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.popOperand()); + checkCalc(calc, 1.0); + Assertions.assertEquals(1.0, calc.popOperand()); + checkCalc(calc); + } + + @Test + public void testDup_emptyOperandStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().dup()); + } +} diff --git a/modules-template/pom.xml b/modules-template/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f0e8d587a352450f92fbbe56fa7e86b57cf41b60 --- /dev/null +++ b/modules-template/pom.xml @@ -0,0 +1,62 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>it1901</groupId> + <artifactId>modules-template</artifactId> + <version>0.0.1-SNAPSHOT</version> + <packaging>pom</packaging> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>16</maven.compiler.source> + <maven.compiler.target>16</maven.compiler.target> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>5.7.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.7.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>5.7.2</version> + <scope>test</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.1</version> + <configuration> + <release>16</release> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.0.0-M5</version> + </plugin> + </plugins> + </pluginManagement> + </build> + + <modules> + <module>core</module> + <module>ui</module> + </modules> +</project> diff --git a/modules-template/src/main/java/core/Calc.java b/modules-template/src/main/java/core/Calc.java new file mode 100644 index 0000000000000000000000000000000000000000..f50679df535792a6c53eb7d044ff443c2b682bd3 --- /dev/null +++ b/modules-template/src/main/java/core/Calc.java @@ -0,0 +1,108 @@ +package core; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.UnaryOperator; + +public class Calc { + + private final List<Double> operandStack; + + public Calc(double... operands) { + operandStack = new ArrayList<>(operands.length + 2); + for (var d : operands) { + operandStack.add(d); + } + } + + /** + * @return the number of operands on the stack + */ + public int getOperandCount() { + return operandStack.size(); + } + + /** + * Pushes a new operand onto top of the stack. + * @param d the new operand + */ + public void pushOperand(double d) { + operandStack.add(d); + } + + /** + * @param n the place (from the top) to peek + * @return the n'th operand from the top + * @throws IllegalArgumentException if n is larger than the operand count + */ + public double peekOperand(int n) { + if (n > getOperandCount()) { + throw new IllegalArgumentException("Cannot peek at position " + n + " when the operand count is " + getOperandCount()); + } + return operandStack.get(getOperandCount() - n - 1); + } + + /** + * @return the top operand + */ + public double peekOperand() { + return peekOperand(0); + } + + /** + * Removes and returns the top operand. + * @return the top operand + * @throws IllegalStateException if the stack is empty + */ + public double popOperand() { + if (getOperandCount() == 0) { + throw new IllegalStateException("Cannot pop from an empty stack"); + } + return operandStack.remove(operandStack.size() - 1); + } + + /** + * Performs the provided operation in the top operand, and + * replaces it with the result. + * @param op the operation to perform + * @return the result of performing the operation + * @throws IllegalStateException if the operand stack is empty + */ + public double performOperation(UnaryOperator<Double> op) throws IllegalStateException { + // TODO + return 0.0; + } + + /** + * Performs the provided operation in the two topmost operands, and + * replaces them with the result. + * @param op the operation to perform + * @return the result of performing the operation + * @throws IllegalStateException if the operand count is less than two + */ + public double performOperation(BinaryOperator<Double> op) throws IllegalStateException { + if (getOperandCount() < 2) { + throw new IllegalStateException("Too few operands (" + getOperandCount() + ") on the stack"); + } + var op1 = popOperand(); + var op2 = popOperand(); + var result = op.apply(op1, op2); + pushOperand(result); + return result; + } + + /** + * Swaps the two topmost operands. + */ + public void swap() { + // TODO + } + + /** + * Duplicates the top operand. + */ + public void dup() { + // TODO + } +} \ No newline at end of file diff --git a/modules-template/src/test/java/core/CalcTest.java b/modules-template/src/test/java/core/CalcTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dc95b0d25445866ed3bcda4b5ded746d0ac00bff --- /dev/null +++ b/modules-template/src/test/java/core/CalcTest.java @@ -0,0 +1,39 @@ +package core; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CalcTest { + + private static void checkCalc(Calc calc, double... operands) { + Assertions.assertEquals(operands.length, calc.getOperandCount(), "Wrong operand count"); + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], calc.peekOperand(i), "Wrong value at #" + i + " of operand stack"); + } + } + + @Test + public void testCalc() { + checkCalc(new Calc()); + checkCalc(new Calc(1.0), 1.0); + checkCalc(new Calc(3.14, 1.0), 1.0, 3.14); + } + + @Test + public void testPushOperand() { + Calc calc = new Calc(); + calc.pushOperand(1.0); + checkCalc(calc, 1.0); + calc.pushOperand(3.14); + checkCalc(calc, 3.14, 1.0); + } + + @Test + public void testPopOperand() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.popOperand()); + checkCalc(calc, 1.0); + Assertions.assertEquals(1.0, calc.popOperand()); + checkCalc(calc); + } +} diff --git a/modules-template/ui/pom.xml b/modules-template/ui/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..45f4dec3086954adb442313d77ae2edc2c12da57 --- /dev/null +++ b/modules-template/ui/pom.xml @@ -0,0 +1,98 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>it1901</groupId> + <artifactId>modules-ui</artifactId> + + <parent> + <groupId>it1901</groupId> + <artifactId>modules-template</artifactId> + <version>0.0.1-SNAPSHOT</version> + <relativePath>..</relativePath> + </parent> + + <dependencies> + + <dependency> + <groupId>it1901</groupId> + <artifactId>modules-core</artifactId> + <version>0.0.1-SNAPSHOT</version> + </dependency> + + <!-- javafx --> + <dependency> + <groupId>org.openjfx</groupId> + <artifactId>javafx-controls</artifactId> + <version>16</version> + </dependency> + <dependency> + <groupId>org.openjfx</groupId> + <artifactId>javafx-fxml</artifactId> + <version>16</version> + </dependency> + + <!-- junit testing with jupiter --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + </dependency> + + <!-- test javafx with TextFX --> + <dependency> + <groupId>org.testfx</groupId> + <artifactId>testfx-core</artifactId> + <version>4.0.16-alpha</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testfx</groupId> + <artifactId>testfx-junit5</artifactId> + <version>4.0.16-alpha</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest</artifactId> + <version>2.2</version> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + </plugin> + + <plugin> + <groupId>org.openjfx</groupId> + <artifactId>javafx-maven-plugin</artifactId> + <version>0.0.6</version> + <executions> + <execution> + <!-- Default configuration for running --> + <!-- Usage: mvn clean javafx:run --> + <id>default-cli</id> + <configuration> + <mainClass>ui.App</mainClass> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/modules-template/ui/src/main/java/ui/App.java b/modules-template/ui/src/main/java/ui/App.java new file mode 100644 index 0000000000000000000000000000000000000000..acead69e8eac41010e6843e4529e834868b979d3 --- /dev/null +++ b/modules-template/ui/src/main/java/ui/App.java @@ -0,0 +1,27 @@ +package ui; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.io.IOException; + +/** + * JavaFX App + */ +public class App extends Application { + + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("App.fxml")); + Parent parent = fxmlLoader.load(); + stage.setScene(new Scene(parent)); + stage.show(); + } + + public static void main(String[] args) { + launch(); + } +} \ No newline at end of file diff --git a/modules-template/ui/src/main/java/ui/AppController.java b/modules-template/ui/src/main/java/ui/AppController.java new file mode 100644 index 0000000000000000000000000000000000000000..2002d483fceb57bf7da4d4b65f04ee9a8b3f81b1 --- /dev/null +++ b/modules-template/ui/src/main/java/ui/AppController.java @@ -0,0 +1,135 @@ +package ui; + +import core.Calc; + +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.UnaryOperator; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.Labeled; +import javafx.scene.control.ListView; + +public class AppController { + + private Calc calc; + + public AppController() { + calc = new Calc(0.0, 0.0, 0.0); + } + + public Calc getCalc() { + return calc; + } + + public void setCalc(Calc calc) { + this.calc = calc; + updateOperandsView(); + } + + @FXML + private ListView<Double> operandsView; + + @FXML + private Label operandView; + + @FXML + void initialize() { + updateOperandsView(); + } + + private void updateOperandsView() { + List<Double> operands = operandsView.getItems(); + operands.clear(); + int elementCount = Math.min(calc.getOperandCount(), 3); + for (int i = 0; i < elementCount; i++) { + operands.add(calc.peekOperand(elementCount - i - 1)); + } + } + + private String getOperandString() { + return operandView.getText(); + } + + private boolean hasOperand() { + return ! getOperandString().isBlank(); + } + + private double getOperand() { + return Double.valueOf(operandView.getText()); + } + + private void setOperand(String operandString) { + operandView.setText(operandString); + } + + @FXML + void handleEnter() { + if (hasOperand()) { + calc.pushOperand(getOperand()); + } else { + calc.dup(); + } + setOperand(""); + updateOperandsView(); + } + + private void appendToOperand(String s) { + // TODO + } + + @FXML + void handleDigit(ActionEvent ae) { + if (ae.getSource() instanceof Labeled l) { + // TODO append button label to operand + } + } + + @FXML + void handlePoint() { + var operandString = getOperandString(); + if (operandString.contains(".")) { + // TODO remove characters after point + } else { + // TODO append point + } + } + + @FXML + void handleClear() { + // TODO clear operand + } + + @FXML + void handleSwap() { + // TODO clear operand + } + + private void performOperation(UnaryOperator<Double> op) { + // TODO + } + + private void performOperation(boolean swap, BinaryOperator<Double> op) { + if (hasOperand()) { + // TODO push operand first + } + // TODO perform operation, but swap first if needed + } + + @FXML + void handleOpAdd() { + // TODO + } + + @FXML + void handleOpSub() { + // TODO + } + + @FXML + void handleOpMult() { + // TODO + } +} diff --git a/modules-template/ui/src/main/resources/ui/App.fxml b/modules-template/ui/src/main/resources/ui/App.fxml new file mode 100644 index 0000000000000000000000000000000000000000..ff280afc86fa85400985adee515112e7f73247eb --- /dev/null +++ b/modules-template/ui/src/main/resources/ui/App.fxml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.layout.GridPane?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.control.ListView?> + +<GridPane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ui.AppController" + alignment="CENTER" hgap="10.0" vgap="10.0" > + + <ListView fx:id="operandsView" prefHeight="80.0" + GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="4"/> + <Label text="" fx:id="operandView" + GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="4"/> + + <!-- multi-line button label with XML entity for newline --> + <Button text="E n t e r" onAction="#handleEnter" + GridPane.rowIndex="2" GridPane.columnIndex="3" GridPane.rowSpan="3"/> + + <Button text="7" onAction="#handleDigit" + GridPane.rowIndex="2" GridPane.columnIndex="0"/> + <Button text="8" onAction="#handleDigit" + GridPane.rowIndex="2" GridPane.columnIndex="1"/> + <Button text="9" onAction="#handleDigit" + GridPane.rowIndex="2" GridPane.columnIndex="2"/> + + <Button text="4" onAction="#handleDigit" + GridPane.rowIndex="3" GridPane.columnIndex="0"/> + <Button text="5" onAction="#handleDigit" + GridPane.rowIndex="3" GridPane.columnIndex="1"/> + <Button text="6" onAction="#handleDigit" + GridPane.rowIndex="3" GridPane.columnIndex="2"/> + + <Button text="1" onAction="#handleDigit" + GridPane.rowIndex="4" GridPane.columnIndex="0"/> + <Button text="2" onAction="#handleDigit" + GridPane.rowIndex="4" GridPane.columnIndex="1"/> + <Button text="3" onAction="#handleDigit" + GridPane.rowIndex="4" GridPane.columnIndex="2"/> + + <Button text="0" onAction="#handleDigit" + GridPane.rowIndex="5" GridPane.columnIndex="0"/> + <Button text="." onAction="#handlePoint" + GridPane.rowIndex="5" GridPane.columnIndex="1"/> + <Button text="C" onAction="#handleClear" + GridPane.rowIndex="5" GridPane.columnIndex="2"/> + <Button text="~" onAction="#handleSwap" + GridPane.rowIndex="5" GridPane.columnIndex="3"/> + + <Button text="+" onAction="#handleOpAdd" + GridPane.rowIndex="6" GridPane.columnIndex="0"/> + <Button text="-" onAction="#handleOpSub" + GridPane.rowIndex="6" GridPane.columnIndex="1"/> + <Button text="*" onAction="#handleOpMult" + GridPane.rowIndex="6" GridPane.columnIndex="2"/> + + <!-- TODO --> + <Button text="/" + GridPane.rowIndex="6" GridPane.columnIndex="3"/> + <Button text="√" + GridPane.rowIndex="7" GridPane.columnIndex="0"/> + <Button text="π" + GridPane.rowIndex="7" GridPane.columnIndex="1"/> +</GridPane> diff --git a/modules-template/ui/src/test/java/ui/AppTest.java b/modules-template/ui/src/test/java/ui/AppTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3c6e9917b8cd6957b969f3ea2dfd694df77a3f94 --- /dev/null +++ b/modules-template/ui/src/test/java/ui/AppTest.java @@ -0,0 +1,130 @@ +package ui; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.testfx.framework.junit5.ApplicationTest; +import org.testfx.matcher.control.LabeledMatchers; + +/** + * TestFX App test + */ +public class AppTest extends ApplicationTest { + + private AppController controller; + private Parent root; + + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("App.fxml")); + root = fxmlLoader.load(); + controller = fxmlLoader.getController(); + stage.setScene(new Scene(root)); + stage.show(); + } + + public Parent getRootNode() { + return root; + } + + private String enterLabel = """ + E + n + t + e + r + """.stripTrailing(); + + private void click(String... labels) { + for (var label : labels) { + clickOn(LabeledMatchers.hasText(label)); + } + } + + private String getOperandString() { + return ((Label) getRootNode().lookup("#operandView")).getText(); + } + + private ListView<Double> getOperandsView() { + return (ListView<Double>) getRootNode().lookup("#operandsView"); + } + + private void checkView(double... operands) { + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], controller.getCalc().peekOperand(i), "Wrong value at #" + i + " of operand stack"); + } + List<Double> viewItems = getOperandsView().getItems(); + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], viewItems.get(viewItems.size() - i - 1), "Wrong value at #" + i + " of operands view"); + } + } + + private void checkView(String operandString, double... operands) { + Assertions.assertEquals(operandString, getOperandString()); + checkView(operands); + } + + // see https://www.baeldung.com/parameterized-tests-junit-5 + // about @ParameterizedTest + + @ParameterizedTest + @MethodSource + public void testClicksOperand(String labels, String operandString) { + for (var label : labels.split(" ")) { + click(label); + } + checkView(operandString); + } + + private static Stream<Arguments> testClicksOperand() { + return Stream.of( + Arguments.of("2 7", "27"), + Arguments.of("2 7 .", "27."), + Arguments.of("2 7 . 5", "27.5"), + Arguments.of("2 7 . 5 .", "27.") + ); + } + + @ParameterizedTest + @MethodSource + public void testClicksOperands(String labels, String operandsString) { + for (var label : labels.split(" ")) { + click(label.equals("\n") ? enterLabel : label); + } + checkView("", Stream.of(operandsString.split(" ")).mapToDouble(Double::valueOf).toArray()); + } + + private static Stream<Arguments> testClicksOperands() { + return Stream.of( + Arguments.of("2 7 . 5 \n", "27.5"), + Arguments.of("2 7 \n", "27.0"), + Arguments.of("2 \n 7 \n 5 \n", "5.0", "7.0", "2.0"), + Arguments.of("2 7 . \n", "27.0"), + Arguments.of("2 7 . 5 \n", "27.5"), + Arguments.of("2 \n 7 +", "9.0"), + Arguments.of("2 \n 7 -", "-5.0"), + Arguments.of("2 \n 7 *", "14.0"), + Arguments.of("6 \n 3 /", "2.0"), + Arguments.of("2 5 \n √", "5.0") + ); + } + + @Test + public void testPi() { + click("π"); + checkView("", Math.PI); + } +} diff --git a/modules-template/ui/src/test/java/ui/README.md b/modules-template/ui/src/test/java/ui/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0cc59802a04720e2dfddaa5c14c029328b91ef33 --- /dev/null +++ b/modules-template/ui/src/test/java/ui/README.md @@ -0,0 +1,38 @@ +# Tests for the RPN calculator + +This folder/package contains tests based on TestFX for the RPN Calculator (currently only one test class). + +As can be seen when launching, the app contains a list (top) showing the operands +(topmost operand at the bottom), a text field (below list, initially empty) for a new operand and +the buttons for digits, enter, decimal point, operations etc. + +## What is tested + +The tests simulate clicks on the buttons and checks that the underlying Calc object, +the list (a view of the Calc object's operand stack) and the text field are updated as expected. +E.g. if you click buttons `2 3 . 5` the string `23.5` should be shown, +while the list is not affected. If you then click `enter`, the text field should be emptied, the operand stack should have `23.5` at the top and the list should have `23.5` at the bottom +(logically the top of the operand stack). + +Below are the specific cases that are tested. + +buttons to click `=>` text field content: + +- `2 7` => `27` +- `2 7 .` => `27.` +- `2 7 . 5` => `27.5` +- `2 7 . 5 .` => `27.` (cut at decimal point) + +buttons to click `=>` operand stack/list content (from the bottom): + +- `2 7 . 5 enter"` => `27.5` +- `2 7 enter` => `27.0"` +- `2 enter 7 enter 5 enter` => `5.0 7.0 2.0` +- `2 7 . enter` => `27.0` +- `2 7 . 5 enter` => `27.5` +- `2 enter 7 +` => `9.0` +- `2 enter 7 -` => `-5.0` +- `2 enter 7 *` => `14.0` +- `6 enter 3 /` => `2.0` +- `2 5 enter √` => `5.0` +- `π` => `3.1415...` (the value of the `Math.PI` constant) diff --git a/packages-template/.gitignore b/packages-template/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..213163975bb915006004bbbf5a9c7d75f6598f95 --- /dev/null +++ b/packages-template/.gitignore @@ -0,0 +1,2 @@ +# ignore maven build folder +target/ diff --git a/packages-template/pom.xml b/packages-template/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ccbf72dbe2307556270dc7c6dd6d2d6108431df5 --- /dev/null +++ b/packages-template/pom.xml @@ -0,0 +1,103 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>it1901</groupId> + <artifactId>packages-template</artifactId> + <version>0.0.1-SNAPSHOT</version> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>16</maven.compiler.source> + <maven.compiler.target>16</maven.compiler.target> + </properties> + + <dependencies> + + <!-- javafx --> + <dependency> + <groupId>org.openjfx</groupId> + <artifactId>javafx-controls</artifactId> + <version>16</version> + </dependency> + <dependency> + <groupId>org.openjfx</groupId> + <artifactId>javafx-fxml</artifactId> + <version>16</version> + </dependency> + + <!-- junit testing with jupiter --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>5.7.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.7.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>5.7.2</version> + <scope>test</scope> + </dependency> + + <!-- test javafx with TextFX --> + <dependency> + <groupId>org.testfx</groupId> + <artifactId>testfx-core</artifactId> + <version>4.0.16-alpha</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testfx</groupId> + <artifactId>testfx-junit5</artifactId> + <version>4.0.16-alpha</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest</artifactId> + <version>2.2</version> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.1</version> + <configuration> + <release>16</release> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.0.0-M5</version> + </plugin> + + <plugin> + <groupId>org.openjfx</groupId> + <artifactId>javafx-maven-plugin</artifactId> + <version>0.0.6</version> + <executions> + <execution> + <!-- Default configuration for running --> + <!-- Usage: mvn clean javafx:run --> + <id>default-cli</id> + <configuration> + <mainClass>ui.App</mainClass> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/packages-template/src/main/java/core/Calc.java b/packages-template/src/main/java/core/Calc.java new file mode 100644 index 0000000000000000000000000000000000000000..f50679df535792a6c53eb7d044ff443c2b682bd3 --- /dev/null +++ b/packages-template/src/main/java/core/Calc.java @@ -0,0 +1,108 @@ +package core; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.UnaryOperator; + +public class Calc { + + private final List<Double> operandStack; + + public Calc(double... operands) { + operandStack = new ArrayList<>(operands.length + 2); + for (var d : operands) { + operandStack.add(d); + } + } + + /** + * @return the number of operands on the stack + */ + public int getOperandCount() { + return operandStack.size(); + } + + /** + * Pushes a new operand onto top of the stack. + * @param d the new operand + */ + public void pushOperand(double d) { + operandStack.add(d); + } + + /** + * @param n the place (from the top) to peek + * @return the n'th operand from the top + * @throws IllegalArgumentException if n is larger than the operand count + */ + public double peekOperand(int n) { + if (n > getOperandCount()) { + throw new IllegalArgumentException("Cannot peek at position " + n + " when the operand count is " + getOperandCount()); + } + return operandStack.get(getOperandCount() - n - 1); + } + + /** + * @return the top operand + */ + public double peekOperand() { + return peekOperand(0); + } + + /** + * Removes and returns the top operand. + * @return the top operand + * @throws IllegalStateException if the stack is empty + */ + public double popOperand() { + if (getOperandCount() == 0) { + throw new IllegalStateException("Cannot pop from an empty stack"); + } + return operandStack.remove(operandStack.size() - 1); + } + + /** + * Performs the provided operation in the top operand, and + * replaces it with the result. + * @param op the operation to perform + * @return the result of performing the operation + * @throws IllegalStateException if the operand stack is empty + */ + public double performOperation(UnaryOperator<Double> op) throws IllegalStateException { + // TODO + return 0.0; + } + + /** + * Performs the provided operation in the two topmost operands, and + * replaces them with the result. + * @param op the operation to perform + * @return the result of performing the operation + * @throws IllegalStateException if the operand count is less than two + */ + public double performOperation(BinaryOperator<Double> op) throws IllegalStateException { + if (getOperandCount() < 2) { + throw new IllegalStateException("Too few operands (" + getOperandCount() + ") on the stack"); + } + var op1 = popOperand(); + var op2 = popOperand(); + var result = op.apply(op1, op2); + pushOperand(result); + return result; + } + + /** + * Swaps the two topmost operands. + */ + public void swap() { + // TODO + } + + /** + * Duplicates the top operand. + */ + public void dup() { + // TODO + } +} \ No newline at end of file diff --git a/packages-template/src/main/java/ui/App.java b/packages-template/src/main/java/ui/App.java new file mode 100644 index 0000000000000000000000000000000000000000..acead69e8eac41010e6843e4529e834868b979d3 --- /dev/null +++ b/packages-template/src/main/java/ui/App.java @@ -0,0 +1,27 @@ +package ui; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.io.IOException; + +/** + * JavaFX App + */ +public class App extends Application { + + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("App.fxml")); + Parent parent = fxmlLoader.load(); + stage.setScene(new Scene(parent)); + stage.show(); + } + + public static void main(String[] args) { + launch(); + } +} \ No newline at end of file diff --git a/packages-template/src/main/java/ui/AppController.java b/packages-template/src/main/java/ui/AppController.java new file mode 100644 index 0000000000000000000000000000000000000000..2002d483fceb57bf7da4d4b65f04ee9a8b3f81b1 --- /dev/null +++ b/packages-template/src/main/java/ui/AppController.java @@ -0,0 +1,135 @@ +package ui; + +import core.Calc; + +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.UnaryOperator; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.Labeled; +import javafx.scene.control.ListView; + +public class AppController { + + private Calc calc; + + public AppController() { + calc = new Calc(0.0, 0.0, 0.0); + } + + public Calc getCalc() { + return calc; + } + + public void setCalc(Calc calc) { + this.calc = calc; + updateOperandsView(); + } + + @FXML + private ListView<Double> operandsView; + + @FXML + private Label operandView; + + @FXML + void initialize() { + updateOperandsView(); + } + + private void updateOperandsView() { + List<Double> operands = operandsView.getItems(); + operands.clear(); + int elementCount = Math.min(calc.getOperandCount(), 3); + for (int i = 0; i < elementCount; i++) { + operands.add(calc.peekOperand(elementCount - i - 1)); + } + } + + private String getOperandString() { + return operandView.getText(); + } + + private boolean hasOperand() { + return ! getOperandString().isBlank(); + } + + private double getOperand() { + return Double.valueOf(operandView.getText()); + } + + private void setOperand(String operandString) { + operandView.setText(operandString); + } + + @FXML + void handleEnter() { + if (hasOperand()) { + calc.pushOperand(getOperand()); + } else { + calc.dup(); + } + setOperand(""); + updateOperandsView(); + } + + private void appendToOperand(String s) { + // TODO + } + + @FXML + void handleDigit(ActionEvent ae) { + if (ae.getSource() instanceof Labeled l) { + // TODO append button label to operand + } + } + + @FXML + void handlePoint() { + var operandString = getOperandString(); + if (operandString.contains(".")) { + // TODO remove characters after point + } else { + // TODO append point + } + } + + @FXML + void handleClear() { + // TODO clear operand + } + + @FXML + void handleSwap() { + // TODO clear operand + } + + private void performOperation(UnaryOperator<Double> op) { + // TODO + } + + private void performOperation(boolean swap, BinaryOperator<Double> op) { + if (hasOperand()) { + // TODO push operand first + } + // TODO perform operation, but swap first if needed + } + + @FXML + void handleOpAdd() { + // TODO + } + + @FXML + void handleOpSub() { + // TODO + } + + @FXML + void handleOpMult() { + // TODO + } +} diff --git a/packages-template/src/main/resources/ui/App.fxml b/packages-template/src/main/resources/ui/App.fxml new file mode 100644 index 0000000000000000000000000000000000000000..ff280afc86fa85400985adee515112e7f73247eb --- /dev/null +++ b/packages-template/src/main/resources/ui/App.fxml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.layout.GridPane?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.control.ListView?> + +<GridPane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ui.AppController" + alignment="CENTER" hgap="10.0" vgap="10.0" > + + <ListView fx:id="operandsView" prefHeight="80.0" + GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="4"/> + <Label text="" fx:id="operandView" + GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="4"/> + + <!-- multi-line button label with XML entity for newline --> + <Button text="E n t e r" onAction="#handleEnter" + GridPane.rowIndex="2" GridPane.columnIndex="3" GridPane.rowSpan="3"/> + + <Button text="7" onAction="#handleDigit" + GridPane.rowIndex="2" GridPane.columnIndex="0"/> + <Button text="8" onAction="#handleDigit" + GridPane.rowIndex="2" GridPane.columnIndex="1"/> + <Button text="9" onAction="#handleDigit" + GridPane.rowIndex="2" GridPane.columnIndex="2"/> + + <Button text="4" onAction="#handleDigit" + GridPane.rowIndex="3" GridPane.columnIndex="0"/> + <Button text="5" onAction="#handleDigit" + GridPane.rowIndex="3" GridPane.columnIndex="1"/> + <Button text="6" onAction="#handleDigit" + GridPane.rowIndex="3" GridPane.columnIndex="2"/> + + <Button text="1" onAction="#handleDigit" + GridPane.rowIndex="4" GridPane.columnIndex="0"/> + <Button text="2" onAction="#handleDigit" + GridPane.rowIndex="4" GridPane.columnIndex="1"/> + <Button text="3" onAction="#handleDigit" + GridPane.rowIndex="4" GridPane.columnIndex="2"/> + + <Button text="0" onAction="#handleDigit" + GridPane.rowIndex="5" GridPane.columnIndex="0"/> + <Button text="." onAction="#handlePoint" + GridPane.rowIndex="5" GridPane.columnIndex="1"/> + <Button text="C" onAction="#handleClear" + GridPane.rowIndex="5" GridPane.columnIndex="2"/> + <Button text="~" onAction="#handleSwap" + GridPane.rowIndex="5" GridPane.columnIndex="3"/> + + <Button text="+" onAction="#handleOpAdd" + GridPane.rowIndex="6" GridPane.columnIndex="0"/> + <Button text="-" onAction="#handleOpSub" + GridPane.rowIndex="6" GridPane.columnIndex="1"/> + <Button text="*" onAction="#handleOpMult" + GridPane.rowIndex="6" GridPane.columnIndex="2"/> + + <!-- TODO --> + <Button text="/" + GridPane.rowIndex="6" GridPane.columnIndex="3"/> + <Button text="√" + GridPane.rowIndex="7" GridPane.columnIndex="0"/> + <Button text="π" + GridPane.rowIndex="7" GridPane.columnIndex="1"/> +</GridPane> diff --git a/packages-template/src/test/java/core/CalcTest.java b/packages-template/src/test/java/core/CalcTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0207cd5a5d7ed3f9a44bb1e3a159d33c91f8dacb --- /dev/null +++ b/packages-template/src/test/java/core/CalcTest.java @@ -0,0 +1,114 @@ +package core; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CalcTest { + + private static void checkCalc(Calc calc, double... operands) { + Assertions.assertEquals(operands.length, calc.getOperandCount(), "Wrong operand count"); + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], calc.peekOperand(i), "Wrong value at #" + i + " of operand stack"); + } + } + + @Test + public void testCalc() { + checkCalc(new Calc()); + checkCalc(new Calc(1.0), 1.0); + checkCalc(new Calc(3.14, 1.0), 1.0, 3.14); + } + + @Test + public void testPushOperand() { + Calc calc = new Calc(); + calc.pushOperand(1.0); + checkCalc(calc, 1.0); + calc.pushOperand(3.14); + checkCalc(calc, 3.14, 1.0); + } + + @Test + public void testPeekOperand() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.peekOperand()); + Assertions.assertThrows(IllegalArgumentException.class, () -> new Calc().peekOperand()); + } + + @Test + public void testPeekOperandN() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.peekOperand(0)); + Assertions.assertEquals(1.0, calc.peekOperand(1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> calc.peekOperand(2)); + } + + @Test + public void testPopOperand() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.popOperand()); + checkCalc(calc, 1.0); + Assertions.assertEquals(1.0, calc.popOperand()); + checkCalc(calc); + } + + @Test + public void testPopOperand_emptyStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().popOperand()); + } + + @Test + public void testPerformOperation1() { + Calc calc = new Calc(1.0); + Assertions.assertEquals(-1.0, calc.performOperation(n -> -n)); + checkCalc(calc, -1.0); + } + + @Test + public void testPerformOperation1_emptyOperandStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation(n -> -n)); + } + + + @Test + public void testPerformOperation2() { + Calc calc = new Calc(1.0, 3.0); + Assertions.assertEquals(-2.0, calc.performOperation((n1, n2) -> n1 - n2)); + checkCalc(calc, -2.0); + } + + @Test + public void testPerformOperation2_lessThanTwoOperands() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).performOperation((n1, n2) -> n1 - n2)); + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation((n1, n2) -> n1 - n2)); + } + + @Test + public void testSwap() { + Calc calc = new Calc(1.0, 3.14); + calc.swap(); + checkCalc(calc, 3.14, 1.0); + calc.swap(); + checkCalc(calc, 1.0, 3.14); + } + + @Test + public void testSwap_lessThanTwoOperands() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).swap()); + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().swap()); + } + + @Test + public void testDup() { + Calc calc = new Calc(1.0, 3.14); + Assertions.assertEquals(3.14, calc.popOperand()); + checkCalc(calc, 1.0); + Assertions.assertEquals(1.0, calc.popOperand()); + checkCalc(calc); + } + + @Test + public void testDup_emptyOperandStack() { + Assertions.assertThrows(IllegalStateException.class, () -> new Calc().dup()); + } +} diff --git a/packages-template/src/test/java/ui/AppTest.java b/packages-template/src/test/java/ui/AppTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3c6e9917b8cd6957b969f3ea2dfd694df77a3f94 --- /dev/null +++ b/packages-template/src/test/java/ui/AppTest.java @@ -0,0 +1,130 @@ +package ui; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.testfx.framework.junit5.ApplicationTest; +import org.testfx.matcher.control.LabeledMatchers; + +/** + * TestFX App test + */ +public class AppTest extends ApplicationTest { + + private AppController controller; + private Parent root; + + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("App.fxml")); + root = fxmlLoader.load(); + controller = fxmlLoader.getController(); + stage.setScene(new Scene(root)); + stage.show(); + } + + public Parent getRootNode() { + return root; + } + + private String enterLabel = """ + E + n + t + e + r + """.stripTrailing(); + + private void click(String... labels) { + for (var label : labels) { + clickOn(LabeledMatchers.hasText(label)); + } + } + + private String getOperandString() { + return ((Label) getRootNode().lookup("#operandView")).getText(); + } + + private ListView<Double> getOperandsView() { + return (ListView<Double>) getRootNode().lookup("#operandsView"); + } + + private void checkView(double... operands) { + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], controller.getCalc().peekOperand(i), "Wrong value at #" + i + " of operand stack"); + } + List<Double> viewItems = getOperandsView().getItems(); + for (int i = 0; i < operands.length; i++) { + Assertions.assertEquals(operands[i], viewItems.get(viewItems.size() - i - 1), "Wrong value at #" + i + " of operands view"); + } + } + + private void checkView(String operandString, double... operands) { + Assertions.assertEquals(operandString, getOperandString()); + checkView(operands); + } + + // see https://www.baeldung.com/parameterized-tests-junit-5 + // about @ParameterizedTest + + @ParameterizedTest + @MethodSource + public void testClicksOperand(String labels, String operandString) { + for (var label : labels.split(" ")) { + click(label); + } + checkView(operandString); + } + + private static Stream<Arguments> testClicksOperand() { + return Stream.of( + Arguments.of("2 7", "27"), + Arguments.of("2 7 .", "27."), + Arguments.of("2 7 . 5", "27.5"), + Arguments.of("2 7 . 5 .", "27.") + ); + } + + @ParameterizedTest + @MethodSource + public void testClicksOperands(String labels, String operandsString) { + for (var label : labels.split(" ")) { + click(label.equals("\n") ? enterLabel : label); + } + checkView("", Stream.of(operandsString.split(" ")).mapToDouble(Double::valueOf).toArray()); + } + + private static Stream<Arguments> testClicksOperands() { + return Stream.of( + Arguments.of("2 7 . 5 \n", "27.5"), + Arguments.of("2 7 \n", "27.0"), + Arguments.of("2 \n 7 \n 5 \n", "5.0", "7.0", "2.0"), + Arguments.of("2 7 . \n", "27.0"), + Arguments.of("2 7 . 5 \n", "27.5"), + Arguments.of("2 \n 7 +", "9.0"), + Arguments.of("2 \n 7 -", "-5.0"), + Arguments.of("2 \n 7 *", "14.0"), + Arguments.of("6 \n 3 /", "2.0"), + Arguments.of("2 5 \n √", "5.0") + ); + } + + @Test + public void testPi() { + click("π"); + checkView("", Math.PI); + } +} diff --git a/packages-template/src/test/java/ui/README.md b/packages-template/src/test/java/ui/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0cc59802a04720e2dfddaa5c14c029328b91ef33 --- /dev/null +++ b/packages-template/src/test/java/ui/README.md @@ -0,0 +1,38 @@ +# Tests for the RPN calculator + +This folder/package contains tests based on TestFX for the RPN Calculator (currently only one test class). + +As can be seen when launching, the app contains a list (top) showing the operands +(topmost operand at the bottom), a text field (below list, initially empty) for a new operand and +the buttons for digits, enter, decimal point, operations etc. + +## What is tested + +The tests simulate clicks on the buttons and checks that the underlying Calc object, +the list (a view of the Calc object's operand stack) and the text field are updated as expected. +E.g. if you click buttons `2 3 . 5` the string `23.5` should be shown, +while the list is not affected. If you then click `enter`, the text field should be emptied, the operand stack should have `23.5` at the top and the list should have `23.5` at the bottom +(logically the top of the operand stack). + +Below are the specific cases that are tested. + +buttons to click `=>` text field content: + +- `2 7` => `27` +- `2 7 .` => `27.` +- `2 7 . 5` => `27.5` +- `2 7 . 5 .` => `27.` (cut at decimal point) + +buttons to click `=>` operand stack/list content (from the bottom): + +- `2 7 . 5 enter"` => `27.5` +- `2 7 enter` => `27.0"` +- `2 enter 7 enter 5 enter` => `5.0 7.0 2.0` +- `2 7 . enter` => `27.0` +- `2 7 . 5 enter` => `27.5` +- `2 enter 7 +` => `9.0` +- `2 enter 7 -` => `-5.0` +- `2 enter 7 *` => `14.0` +- `6 enter 3 /` => `2.0` +- `2 5 enter √` => `5.0` +- `π` => `3.1415...` (the value of the `Math.PI` constant)