From 64a7c8f49737d1b90ff5277ea6a1f1b89aaef42c Mon Sep 17 00:00:00 2001 From: pankajppurohit Date: Mon, 17 Feb 2025 23:10:00 +0530 Subject: [PATCH 1/2] Added java assignment for java berlin clock, with build fixes and exception handling --- .gitignore | 36 ++++++++ build.gradle | 22 ++--- gradle/wrapper/gradle-wrapper.properties | 2 +- instructions/build.gradle | 12 +-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- instructions/src/test/resources/log4j.xml | 2 +- .../com/ubs/opsit/interviews/BerlinClock.java | 89 +++++++++++++++++++ .../ubs/opsit/interviews/BerlinClockMain.java | 13 +++ .../com/ubs/opsit/interviews/LampSymbol.java | 10 +++ .../opsit/interviews/BerlinClockFixture.java | 25 ++++-- .../support/BehaviouralTestEmbedder.java | 8 +- .../support/ClasspathStoryFinder.java | 10 +-- src/test/resources/log4j.xml | 2 +- src/test/resources/stories/berlin-clock.story | 28 ++++++ 14 files changed, 227 insertions(+), 34 deletions(-) create mode 100644 .gitignore create mode 100644 src/main/java/com/ubs/opsit/interviews/BerlinClock.java create mode 100644 src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java create mode 100644 src/main/java/com/ubs/opsit/interviews/LampSymbol.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..040b81a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +### Gradle files ### +.gradle/ +build/ +!gradle-wrapper.jar +!gradle-wrapper.properties + +### IntelliJ IDEA files ### +.idea/ +*.iml +*.iws +*.ipr + +### OS-specific files ### +.DS_Store +Thumbs.db + +### Log files ### +*.log + +### Temporary files ### +*.swp +*.swo +*.bak + +### Gradle Wrapper ### +gradle-wrapper.jar +gradle-wrapper.properties + +### Compiled class files ### +*.class + +### JetBrains Rider ### +.idea/.idea_modules/ + +### IntelliJ project files ### +out/ diff --git a/build.gradle b/build.gradle index efba2e7..1ca35a1 100644 --- a/build.gradle +++ b/build.gradle @@ -7,13 +7,12 @@ apply plugin: 'eclipse' version = '1.0-SNAPSHOT' group = 'org.suggs.interviews.berlinclock' -task wrapper(type: Wrapper){ +wrapper { description = 'Generates gradlew scripts for NIX and win envs' - gradleVersion = '2.0' + gradleVersion = '7.3' } repositories { - jcenter() mavenCentral() } @@ -22,15 +21,18 @@ idea.module { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.5', + implementation 'org.slf4j:slf4j-api:1.7.5', 'commons-lang:commons-lang:2.6' - runtime 'org.slf4j:slf4j-log4j12:1.7.5', + implementation 'org.slf4j:slf4j-log4j12:1.7.5', 'log4j:log4j:1.2.17' - testCompile 'junit:junit:4.11', - 'org.mockito:mockito-core:1.9.5', - 'org.assertj:assertj-core:1.6.1', - 'commons-io:commons-io:2.4', - 'org.jbehave:jbehave-core:3.8' + testImplementation 'org.mockito:mockito-core:5.15.2', + 'org.assertj:assertj-core:3.27.3', + 'commons-io:commons-io:2.18.0', + 'org.jbehave:jbehave-core:5.2.0' + // Jupiter api and engine dependencies + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.3' + } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6a0e65d..2addbc9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/instructions/build.gradle b/instructions/build.gradle index 5a3009b..b77d5e6 100644 --- a/instructions/build.gradle +++ b/instructions/build.gradle @@ -7,13 +7,13 @@ apply plugin: 'eclipse' version = '1.0-SNAPSHOT' group = 'org.suggs.interviews.example' -task wrapper(type: Wrapper){ +wrapper { description = 'Generates gradlew scripts for NIX and win envs' - gradleVersion = '2.0' + gradleVersion = '7.3' } repositories { - jcenter() + mavenCentral() mavenLocal() } @@ -22,10 +22,10 @@ idea.module { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.5' + implementation 'org.slf4j:slf4j-api:1.7.5' - runtime 'org.slf4j:slf4j-log4j12:1.7.5', + implementation 'org.slf4j:slf4j-log4j12:1.7.5', 'log4j:log4j:1.2.17' - testCompile 'junit:junit:4.11' + testImplementation 'junit:junit:4.11' } diff --git a/instructions/gradle/wrapper/gradle-wrapper.properties b/instructions/gradle/wrapper/gradle-wrapper.properties index 74e3e0c..e77ad86 100644 --- a/instructions/gradle/wrapper/gradle-wrapper.properties +++ b/instructions/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/instructions/src/test/resources/log4j.xml b/instructions/src/test/resources/log4j.xml index 9402a1c..0478763 100644 --- a/instructions/src/test/resources/log4j.xml +++ b/instructions/src/test/resources/log4j.xml @@ -1,5 +1,5 @@ - + diff --git a/src/main/java/com/ubs/opsit/interviews/BerlinClock.java b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java new file mode 100644 index 0000000..858d55f --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java @@ -0,0 +1,89 @@ +package com.ubs.opsit.interviews; + +import java.util.Objects; + +/** + * @author Pankaj + */ +public class BerlinClock implements TimeConverter { + @Override + public String convertTime(String time) { + if (time == null || time.isEmpty()) { + throw new IllegalArgumentException("Invalid time format"); + } + + // Handle invalid time format + String[] values = time.split(":"); + if (values.length != 3) { + throw new IllegalArgumentException("Invalid time format"); + } + + try { + int hours = Integer.parseInt(values[0]); + int minutes = Integer.parseInt(values[1]); + int seconds = Integer.parseInt(values[2]); + + // Validate hours, minutes, and seconds range + if (hours < 0 || hours > 24 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) { + throw new IllegalArgumentException("Invalid time format"); + } + + // If hours == 24, check if minutes and seconds are 00 + if (hours == 24 && (minutes != 0 || seconds != 0)) { + throw new IllegalArgumentException("Invalid time format"); + } + + // Construct the Berlin Clock string + return getLampOnOff(seconds) + " " + getHours(hours) + " " + getMinutes(minutes); + + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid time format"); + } + } + + + /** + * Every 2 seconds lamp 1st row blinks on/off + * @param seconds + * @return + */ + protected String getLampOnOff(int seconds) { + return seconds % 2 == 0 ? LampSymbol.Y.name() : LampSymbol.O.name(); + } + + protected String getHours(int hours) { + int numberTopHourLamps = hours / 5; + int numberBottomHourLamps = hours % 5; + + return getLampRow(4, numberTopHourLamps, LampSymbol.R) + " " + getLampRow(4, numberBottomHourLamps, LampSymbol.R); + } + + protected String getMinutes(int minutes) { + int numberTopMinutesLamps = minutes / 5; + int numberBottomMinutesLamps = minutes % 5; + + StringBuilder sb = new StringBuilder(); + + for (int i = 1; i <= 11; i++) { + sb.append(i <= numberTopMinutesLamps ? getMinuteLampColour(i) : LampSymbol.O.name()); + } + + sb.append(" "); + + sb.append(getLampRow(4, numberBottomMinutesLamps, LampSymbol.Y)); + + return sb.toString(); + } + + private String getLampRow(int totalNumberLamps, int numberLampsOn, LampSymbol lampSymbol) { + StringBuilder sb = new StringBuilder(totalNumberLamps); + for (int i = 0; i < totalNumberLamps; i++) { + sb.append(i < numberLampsOn ? lampSymbol : LampSymbol.O.name()); + } + return sb.toString(); + } + + private String getMinuteLampColour(int index) { + return index % 3 == 0 ? LampSymbol.R.name() : LampSymbol.Y.name(); + } +} diff --git a/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java b/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java new file mode 100644 index 0000000..3346a72 --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java @@ -0,0 +1,13 @@ +package com.ubs.opsit.interviews; + +public class BerlinClockMain { + + public static void main(String[] args) { + BerlinClock clock = new BerlinClock(); + //clock.convertTime(""); + //clock.convertTime("23.59.59"); + String result = clock.convertTime("23:59:59"); + System.out.println(result); + + } +} diff --git a/src/main/java/com/ubs/opsit/interviews/LampSymbol.java b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java new file mode 100644 index 0000000..f768b70 --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java @@ -0,0 +1,10 @@ +package com.ubs.opsit.interviews; + +/** + * @author Pankaj + */ +public enum LampSymbol { + O, + Y, + R; +} diff --git a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java b/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java index 0310f71..d6beb8d 100644 --- a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java +++ b/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java @@ -2,10 +2,10 @@ import org.jbehave.core.annotations.Then; import org.jbehave.core.annotations.When; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import static com.ubs.opsit.interviews.support.BehaviouralTestEmbedder.aBehaviouralTestRunner; -import static org.assertj.core.api.Assertions.assertThat; /** * Acceptance test class that uses the JBehave (Gerkin) syntax for writing stories. You should not need to @@ -13,14 +13,14 @@ */ public class BerlinClockFixture { - private TimeConverter berlinClock; + private TimeConverter berlinClock = new BerlinClock(); // Initialize BerlinClock private String theTime; @Test public void berlinClockAcceptanceTests() throws Exception { aBehaviouralTestRunner() .usingStepsFrom(this) - .withStory("berlin-clock.story") + .withStory("berlin-clock.story") // The story file that contains scenarios .run(); } @@ -31,6 +31,21 @@ public void whenTheTimeIs(String time) { @Then("the clock should look like $") public void thenTheClockShouldLookLike(String theExpectedBerlinClockOutput) { - assertThat(berlinClock.convertTime(theTime)).isEqualTo(theExpectedBerlinClockOutput); + try { + String result = berlinClock.convertTime(theTime); + Assertions.assertEquals(theExpectedBerlinClockOutput, result); + } catch (IllegalArgumentException e) { + Assertions.assertEquals("Invalid time format", e.getMessage()); + } + } + + @Then("the clock should throw an error $") + public void thenTheClockShouldThrowAnError(String expectedError) { + try { + berlinClock.convertTime(theTime); + Assertions.fail("Expected error not thrown"); + } catch (IllegalArgumentException e) { + Assertions.assertEquals(expectedError, e.getMessage()); + } } } diff --git a/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java b/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java index c5abeba..ecf850d 100644 --- a/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java +++ b/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java @@ -9,13 +9,13 @@ import org.jbehave.core.steps.InjectableStepsFactory; import org.jbehave.core.steps.InstanceStepsFactory; import org.jbehave.core.steps.ParameterConverters; +import org.junit.jupiter.api.Assertions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; import static org.jbehave.core.io.CodeLocations.codeLocationFromClass; import static org.jbehave.core.reporters.Format.CONSOLE; import static org.jbehave.core.reporters.Format.HTML; @@ -42,7 +42,7 @@ public static BehaviouralTestEmbedder aBehaviouralTestRunner() { } @Override - public void run() throws Exception { + public void run() { List paths = createStoryPaths(); if (paths == null || paths.isEmpty()) { throw new IllegalStateException("No story paths found for state machine"); @@ -53,7 +53,7 @@ public void run() throws Exception { @Override public InjectableStepsFactory stepsFactory() { - assertThat(stepsFactory).isNotNull(); + Assertions.assertNotNull(stepsFactory); return stepsFactory; } @@ -74,7 +74,7 @@ public BehaviouralTestEmbedder withStory(String aWildcardStoryFilename) { } public BehaviouralTestEmbedder usingStepsFrom(Object... stepsSource) { - assertThat(stepsFactory).isNull(); + Assertions.assertNull(stepsFactory); stepsFactory = new InstanceStepsFactory(configuration(), stepsSource); return this; } diff --git a/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java b/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java index e74713a..133e553 100644 --- a/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java +++ b/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java @@ -1,7 +1,7 @@ package com.ubs.opsit.interviews.support; import org.apache.commons.io.filefilter.DirectoryFileFilter; -import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.apache.commons.io.filefilter.RegexFileFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +23,7 @@ public final class ClasspathStoryFinder { private static final Logger LOG = LoggerFactory.getLogger(ClasspathStoryFinder.class); public static List findFilenamesThatMatch(String aFilenameWithWildcards) { - List filenames = new ArrayList(); + List filenames = new ArrayList<>(); for (File file : findFilesThatMatch(aFilenameWithWildcards)) { filenames.add(file.toURI().toString()); } @@ -31,11 +31,11 @@ public static List findFilenamesThatMatch(String aFilenameWithWildcards) } private static Collection findFilesThatMatch(String aFilenameWithWildcards) { - WildcardFileFilter regexFileFilter = new WildcardFileFilter(aFilenameWithWildcards); + RegexFileFilter regexFileFilter = new RegexFileFilter(aFilenameWithWildcards); List rootDirsToSearchFrom = getRootDirs(); LOG.info("Searching for stories called [{}] in [{}]", aFilenameWithWildcards, rootDirsToSearchFrom); - List ret = new ArrayList() ; + List ret = new ArrayList<>() ; for (File f : rootDirsToSearchFrom) { ret.addAll(listFiles(f, regexFileFilter, DirectoryFileFilter.DIRECTORY)) ; } @@ -43,7 +43,7 @@ private static Collection findFilesThatMatch(String aFilenameWithWildcards } private static List getRootDirs() { - List ret = new ArrayList() ; + List ret = new ArrayList<>() ; try { Enumeration roots = ClasspathStoryFinder.class.getClassLoader().getResources("") ; while(roots.hasMoreElements()) { diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml index cbf5bd8..42ca570 100644 --- a/src/test/resources/log4j.xml +++ b/src/test/resources/log4j.xml @@ -1,5 +1,5 @@ - + diff --git a/src/test/resources/stories/berlin-clock.story b/src/test/resources/stories/berlin-clock.story index 7d64304..8d4d15d 100644 --- a/src/test/resources/stories/berlin-clock.story +++ b/src/test/resources/stories/berlin-clock.story @@ -44,5 +44,33 @@ RRRR OOOOOOOOOOO OOOO +Scenario: Null time + When the time is null + Then the clock should throw an error "Invalid time format" + +Scenario: Empty time value + When the time is "" + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid time format (with dots instead of colons) + When the time is 23.34.34 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid hour value (greater than 24) + When the time is 25:30:45 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid minute value (greater than 59) + When the time is 12:60:30 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid second value (greater than 59) + When the time is 12:30:60 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid time format (non-numeric value) + When the time is "abc:def:ghi" + Then the clock should throw an error "Invalid time format" + From 981c48710bdd64093a38b6a8a34043fb3720ea53 Mon Sep 17 00:00:00 2001 From: pankajppurohit Date: Mon, 24 Feb 2025 02:34:33 +0530 Subject: [PATCH 2/2] Updated the code to use streaming api and added code to support cucumber test framework --- ReadMe | 13 +++ build.gradle | 29 ++++- cucumber-report.html | 48 +++++++++ .../com/ubs/opsit/interviews/BerlinClock.java | 77 ++++++-------- .../ubs/opsit/interviews/BerlinClockMain.java | 13 --- .../com/ubs/opsit/interviews/LampSymbol.java | 23 +++- .../opsit/interviews/BerlinClockFixture.java | 51 --------- .../opsit/interviews/BerlinClockSteps.java | 61 +++++++++++ .../com/ubs/opsit/interviews/TestRunner.java | 13 +++ .../support/BehaviouralTestEmbedder.java | 100 ------------------ .../support/ClasspathStoryFinder.java | 57 ---------- src/test/resources/berlin-clock.feature | 57 ++++++++++ src/test/resources/stories/berlin-clock.story | 76 ------------- 13 files changed, 270 insertions(+), 348 deletions(-) create mode 100644 ReadMe create mode 100644 cucumber-report.html delete mode 100644 src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java delete mode 100644 src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java create mode 100644 src/test/java/com/ubs/opsit/interviews/BerlinClockSteps.java create mode 100644 src/test/java/com/ubs/opsit/interviews/TestRunner.java delete mode 100644 src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java delete mode 100644 src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java create mode 100644 src/test/resources/berlin-clock.feature delete mode 100644 src/test/resources/stories/berlin-clock.story diff --git a/ReadMe b/ReadMe new file mode 100644 index 0000000..e7a8823 --- /dev/null +++ b/ReadMe @@ -0,0 +1,13 @@ +Command to execute test with cucumber CLI plugin: +./gradlew cucumberCli + +Removed this invalid scenario +-------> +Scenario: Midnight (24-hour clock format) +When the time is 24:00:00 +Then the clock should look like +| Y | +| RRRR | +| RRRR | +| OOOOOOOOOOO | +| OOOO | diff --git a/build.gradle b/build.gradle index 1ca35a1..943addd 100644 --- a/build.gradle +++ b/build.gradle @@ -27,12 +27,33 @@ dependencies { implementation 'org.slf4j:slf4j-log4j12:1.7.5', 'log4j:log4j:1.2.17' - testImplementation 'org.mockito:mockito-core:5.15.2', - 'org.assertj:assertj-core:3.27.3', - 'commons-io:commons-io:2.18.0', - 'org.jbehave:jbehave-core:5.2.0' + // Cucumber dependencies + testImplementation 'io.cucumber:cucumber-java:7.11.0' + testImplementation 'io.cucumber:cucumber-junit:7.11.0' + // Jupiter api and engine dependencies testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.3' + testImplementation 'org.junit.vintage:junit-vintage-engine:5.9.3' +} +configurations { + cucumberRuntime { + extendsFrom testImplementation + } } + +task cucumberCli() { + dependsOn assemble, testClasses + doLast { + javaexec { + main = "io.cucumber.core.cli.Main" + classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output + args = [ + '--plugin', 'pretty', + '--plugin', 'html:target/cucumber-report.html', + '--glue', 'com.baeldung.cucumber', + 'src/test/resources'] + } + } +} \ No newline at end of file diff --git a/cucumber-report.html b/cucumber-report.html new file mode 100644 index 0000000..331e30c --- /dev/null +++ b/cucumber-report.html @@ -0,0 +1,48 @@ + + + + Cucumber + + + + + +
+
+ + + + diff --git a/src/main/java/com/ubs/opsit/interviews/BerlinClock.java b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java index 858d55f..5f690e4 100644 --- a/src/main/java/com/ubs/opsit/interviews/BerlinClock.java +++ b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java @@ -1,6 +1,11 @@ package com.ubs.opsit.interviews; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; /** * @author Pankaj @@ -8,39 +13,21 @@ public class BerlinClock implements TimeConverter { @Override public String convertTime(String time) { - if (time == null || time.isEmpty()) { - throw new IllegalArgumentException("Invalid time format"); - } - - // Handle invalid time format - String[] values = time.split(":"); - if (values.length != 3) { + if (Objects.isNull(time)) { throw new IllegalArgumentException("Invalid time format"); } + LocalTime localTime; try { - int hours = Integer.parseInt(values[0]); - int minutes = Integer.parseInt(values[1]); - int seconds = Integer.parseInt(values[2]); - - // Validate hours, minutes, and seconds range - if (hours < 0 || hours > 24 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) { - throw new IllegalArgumentException("Invalid time format"); - } - - // If hours == 24, check if minutes and seconds are 00 - if (hours == 24 && (minutes != 0 || seconds != 0)) { - throw new IllegalArgumentException("Invalid time format"); - } - - // Construct the Berlin Clock string - return getLampOnOff(seconds) + " " + getHours(hours) + " " + getMinutes(minutes); - - } catch (NumberFormatException e) { + localTime = LocalTime.parse(time); + } catch (DateTimeParseException ex) { throw new IllegalArgumentException("Invalid time format"); } - } + return getLampOnOff(localTime.getSecond()) + "\n" + + getHours(localTime.getHour()) + "\n" + + getMinutes(localTime.getMinute()); + } /** * Every 2 seconds lamp 1st row blinks on/off @@ -48,42 +35,44 @@ public String convertTime(String time) { * @return */ protected String getLampOnOff(int seconds) { - return seconds % 2 == 0 ? LampSymbol.Y.name() : LampSymbol.O.name(); + // Use the LampSymbol enum to get the respective values + return (seconds % 2 == 0) ? LampSymbol.YELLOW.getLampValue() : LampSymbol.OFF.getLampValue(); } protected String getHours(int hours) { int numberTopHourLamps = hours / 5; int numberBottomHourLamps = hours % 5; - return getLampRow(4, numberTopHourLamps, LampSymbol.R) + " " + getLampRow(4, numberBottomHourLamps, LampSymbol.R); + // Use the LampSymbol enum for RED lamps + String topHourRow = getLampRowStream(4, numberTopHourLamps, LampSymbol.RED).collect(Collectors.joining()); + String bottomHourRow = getLampRowStream(4, numberBottomHourLamps, LampSymbol.RED).collect(Collectors.joining()); + + return topHourRow + "\n" + bottomHourRow; } protected String getMinutes(int minutes) { int numberTopMinutesLamps = minutes / 5; int numberBottomMinutesLamps = minutes % 5; - StringBuilder sb = new StringBuilder(); + // Using Stream API to generate the first row of lamps (top minutes) + String topMinutesRow = IntStream.rangeClosed(1, 11) + .mapToObj(i -> i <= numberTopMinutesLamps ? getMinuteLampColour(i) : LampSymbol.OFF.getLampValue()) + .collect(Collectors.joining()); - for (int i = 1; i <= 11; i++) { - sb.append(i <= numberTopMinutesLamps ? getMinuteLampColour(i) : LampSymbol.O.name()); - } + // Using Stream API to generate the second row of lamps (bottom minutes) + String bottomMinutesRow = getLampRowStream(4, numberBottomMinutesLamps, LampSymbol.YELLOW) + .collect(Collectors.joining()); - sb.append(" "); - - sb.append(getLampRow(4, numberBottomMinutesLamps, LampSymbol.Y)); - - return sb.toString(); + return topMinutesRow + "\n" + bottomMinutesRow; } - private String getLampRow(int totalNumberLamps, int numberLampsOn, LampSymbol lampSymbol) { - StringBuilder sb = new StringBuilder(totalNumberLamps); - for (int i = 0; i < totalNumberLamps; i++) { - sb.append(i < numberLampsOn ? lampSymbol : LampSymbol.O.name()); - } - return sb.toString(); + private Stream getLampRowStream(int totalNumberLamps, int numberLampsOn, LampSymbol lampSymbol) { + return IntStream.range(0, totalNumberLamps) + .mapToObj(i -> i < numberLampsOn ? lampSymbol.getLampValue() : LampSymbol.OFF.getLampValue()); } private String getMinuteLampColour(int index) { - return index % 3 == 0 ? LampSymbol.R.name() : LampSymbol.Y.name(); + // Use the LampSymbol enum to determine the color of the lamp (Red for multiples of 3) + return (index % 3 == 0) ? LampSymbol.RED.getLampValue() : LampSymbol.YELLOW.getLampValue(); } } diff --git a/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java b/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java deleted file mode 100644 index 3346a72..0000000 --- a/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ubs.opsit.interviews; - -public class BerlinClockMain { - - public static void main(String[] args) { - BerlinClock clock = new BerlinClock(); - //clock.convertTime(""); - //clock.convertTime("23.59.59"); - String result = clock.convertTime("23:59:59"); - System.out.println(result); - - } -} diff --git a/src/main/java/com/ubs/opsit/interviews/LampSymbol.java b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java index f768b70..38dcbd3 100644 --- a/src/main/java/com/ubs/opsit/interviews/LampSymbol.java +++ b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java @@ -4,7 +4,24 @@ * @author Pankaj */ public enum LampSymbol { - O, - Y, - R; + OFF("O"), + YELLOW("Y"), + RED("R"); + + private final String lampValue; + + // Constructor to set the lamp value + LampSymbol(String lampValue) { + this.lampValue = lampValue; + } + + // Method to get the string representation for the lamp symbol + public String getLampValue() { + return this.lampValue; + } + + // Static method to return the lamp symbol as a string based on the enum + public static String getLampSymbol(LampSymbol symbol) { + return symbol.getLampValue(); + } } diff --git a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java b/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java deleted file mode 100644 index d6beb8d..0000000 --- a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ubs.opsit.interviews; - -import org.jbehave.core.annotations.Then; -import org.jbehave.core.annotations.When; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import static com.ubs.opsit.interviews.support.BehaviouralTestEmbedder.aBehaviouralTestRunner; - -/** - * Acceptance test class that uses the JBehave (Gerkin) syntax for writing stories. You should not need to - * edit this class to complete the exercise, this is your definition of done. - */ -public class BerlinClockFixture { - - private TimeConverter berlinClock = new BerlinClock(); // Initialize BerlinClock - private String theTime; - - @Test - public void berlinClockAcceptanceTests() throws Exception { - aBehaviouralTestRunner() - .usingStepsFrom(this) - .withStory("berlin-clock.story") // The story file that contains scenarios - .run(); - } - - @When("the time is $time") - public void whenTheTimeIs(String time) { - theTime = time; - } - - @Then("the clock should look like $") - public void thenTheClockShouldLookLike(String theExpectedBerlinClockOutput) { - try { - String result = berlinClock.convertTime(theTime); - Assertions.assertEquals(theExpectedBerlinClockOutput, result); - } catch (IllegalArgumentException e) { - Assertions.assertEquals("Invalid time format", e.getMessage()); - } - } - - @Then("the clock should throw an error $") - public void thenTheClockShouldThrowAnError(String expectedError) { - try { - berlinClock.convertTime(theTime); - Assertions.fail("Expected error not thrown"); - } catch (IllegalArgumentException e) { - Assertions.assertEquals(expectedError, e.getMessage()); - } - } -} diff --git a/src/test/java/com/ubs/opsit/interviews/BerlinClockSteps.java b/src/test/java/com/ubs/opsit/interviews/BerlinClockSteps.java new file mode 100644 index 0000000..b2ac3fe --- /dev/null +++ b/src/test/java/com/ubs/opsit/interviews/BerlinClockSteps.java @@ -0,0 +1,61 @@ +package com.ubs.opsit.interviews; + +import io.cucumber.java.BeforeAll; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; + +import java.util.List; + +import static org.junit.Assert.*; + +public class BerlinClockSteps { + private static TimeConverter berlinClock; + private String time; + private String errorMessage; + + private IllegalArgumentException exception; + + @BeforeAll + public static void before_or_after_all() { + berlinClock = new BerlinClock(); + } + + @When("the time is {int}:{int}:{int}") + public void the_time_is(Integer int1, Integer int2, Integer int3) { + this.time = String.format("%02d:%02d:%02d", int1, int2, int3); + } + + @Then("the clock should look like") + public void the_clock_should_look_like(List expectedClockResult) { + String[] expectedRows = expectedClockResult.toArray(new String[0]); + //try { + String[] actualRows = berlinClock.convertTime(time).split("\n"); + for (int i = 0; i < expectedRows.length; i++) { + assertEquals(expectedRows[i], actualRows[i]); + } +// } catch (IllegalArgumentException ex) { +// exception = ex; +// } + } + + @When("the invalid time is null") + public void the_time_is_null() { + this.time = null; + } + + @When("the time is {string}") + public void the_time_is_invalid(String time) { + this.time = time; + } + + @When("the time is {double}.{int}") + public void the_time_is(Double double1, Integer int1) { + this.time = time; + } + + @Then("the clock should throw an error {string}") + public void the_clock_should_throw_an_error(String expectedErrorMessage) { + assertThrows(expectedErrorMessage, IllegalArgumentException.class, () -> berlinClock.convertTime(time)); + } + +} diff --git a/src/test/java/com/ubs/opsit/interviews/TestRunner.java b/src/test/java/com/ubs/opsit/interviews/TestRunner.java new file mode 100644 index 0000000..568782d --- /dev/null +++ b/src/test/java/com/ubs/opsit/interviews/TestRunner.java @@ -0,0 +1,13 @@ +package com.ubs.opsit.interviews; + +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions( + plugin = {"pretty", "html:target/cucumber-report.html"}, + features = {"src/test/resources"}, + tags= "@SmokeTest") +public class TestRunner { +} diff --git a/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java b/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java deleted file mode 100644 index ecf850d..0000000 --- a/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.ubs.opsit.interviews.support; - -import org.jbehave.core.ConfigurableEmbedder; -import org.jbehave.core.configuration.Configuration; -import org.jbehave.core.configuration.MostUsefulConfiguration; -import org.jbehave.core.io.LoadFromURL; -import org.jbehave.core.reporters.FilePrintStreamFactory; -import org.jbehave.core.reporters.StoryReporterBuilder; -import org.jbehave.core.steps.InjectableStepsFactory; -import org.jbehave.core.steps.InstanceStepsFactory; -import org.jbehave.core.steps.ParameterConverters; -import org.junit.jupiter.api.Assertions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.SimpleDateFormat; -import java.util.List; - -import static org.jbehave.core.io.CodeLocations.codeLocationFromClass; -import static org.jbehave.core.reporters.Format.CONSOLE; -import static org.jbehave.core.reporters.Format.HTML; - -/** - * A class to fully encapsulates all of the JBehave plumbing behind a builder style API. The expected use for this would be: - * {code}aBehaviouralTestRunner().usingStepsFrom(this).withStory("your.story").run(){code} - * - */ -public final class BehaviouralTestEmbedder extends ConfigurableEmbedder { - - private static final Logger LOG = LoggerFactory.getLogger(BehaviouralTestEmbedder.class); - public static final String BAD_USE_OF_API_MESSAGE = "You are trying to set the steps factory twice ... this is a paradox"; - - private String wildcardStoryFilename; - private InjectableStepsFactory stepsFactory; - - - private BehaviouralTestEmbedder() { - } - - public static BehaviouralTestEmbedder aBehaviouralTestRunner() { - return new BehaviouralTestEmbedder(); - } - - @Override - public void run() { - List paths = createStoryPaths(); - if (paths == null || paths.isEmpty()) { - throw new IllegalStateException("No story paths found for state machine"); - } - LOG.info("Running [" + this.getClass().getSimpleName() + "] with spring_stories [" + paths + "]"); - configuredEmbedder().runStoriesAsPaths(paths); - } - - @Override - public InjectableStepsFactory stepsFactory() { - Assertions.assertNotNull(stepsFactory); - return stepsFactory; - } - - public Configuration configuration() { - return new MostUsefulConfiguration() - .useStoryLoader(new LoadFromURL()) - .useParameterConverters(new ParameterConverters().addConverters(new SandboxDateConverter())) - .useStoryReporterBuilder(new SandboxStoryReporterBuilder()); - } - - private List createStoryPaths() { - return ClasspathStoryFinder.findFilenamesThatMatch(wildcardStoryFilename); - } - - public BehaviouralTestEmbedder withStory(String aWildcardStoryFilename) { - wildcardStoryFilename = aWildcardStoryFilename; - return this; - } - - public BehaviouralTestEmbedder usingStepsFrom(Object... stepsSource) { - Assertions.assertNull(stepsFactory); - stepsFactory = new InstanceStepsFactory(configuration(), stepsSource); - return this; - } - - - static class SandboxDateConverter extends ParameterConverters.DateConverter { - - public SandboxDateConverter() { - super(new SimpleDateFormat("dd-MM-yyyy")); - } - } - - static class SandboxStoryReporterBuilder extends StoryReporterBuilder { - - public SandboxStoryReporterBuilder() { - withCodeLocation(codeLocationFromClass(SandboxStoryReporterBuilder.class)); - withDefaultFormats(); - withFormats(HTML, CONSOLE); - withFailureTrace(true); - withPathResolver(new FilePrintStreamFactory.ResolveToSimpleName()); - } - } -} diff --git a/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java b/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java deleted file mode 100644 index 133e553..0000000 --- a/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.ubs.opsit.interviews.support; - -import org.apache.commons.io.filefilter.DirectoryFileFilter; -import org.apache.commons.io.filefilter.RegexFileFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Enumeration; -import java.util.List; - -import static org.apache.commons.io.FileUtils.listFiles; - -/** - * A class to help us find stories (files) across a classpath with many roots. This is especially important - * when finding files when executed from a Gradle test context. - */ -public final class ClasspathStoryFinder { - private static final Logger LOG = LoggerFactory.getLogger(ClasspathStoryFinder.class); - - public static List findFilenamesThatMatch(String aFilenameWithWildcards) { - List filenames = new ArrayList<>(); - for (File file : findFilesThatMatch(aFilenameWithWildcards)) { - filenames.add(file.toURI().toString()); - } - return filenames; - } - - private static Collection findFilesThatMatch(String aFilenameWithWildcards) { - RegexFileFilter regexFileFilter = new RegexFileFilter(aFilenameWithWildcards); - List rootDirsToSearchFrom = getRootDirs(); - LOG.info("Searching for stories called [{}] in [{}]", aFilenameWithWildcards, rootDirsToSearchFrom); - - List ret = new ArrayList<>() ; - for (File f : rootDirsToSearchFrom) { - ret.addAll(listFiles(f, regexFileFilter, DirectoryFileFilter.DIRECTORY)) ; - } - return ret ; - } - - private static List getRootDirs() { - List ret = new ArrayList<>() ; - try { - Enumeration roots = ClasspathStoryFinder.class.getClassLoader().getResources("") ; - while(roots.hasMoreElements()) { - ret.add(new File(roots.nextElement().getFile())) ; - } - } catch(IOException ioe) { - LOG.error("Failed to derive classpath from Class Loader", ioe) ; - } - return ret ; - } -} diff --git a/src/test/resources/berlin-clock.feature b/src/test/resources/berlin-clock.feature new file mode 100644 index 0000000..e6a3aa9 --- /dev/null +++ b/src/test/resources/berlin-clock.feature @@ -0,0 +1,57 @@ +@SmokeTest +Feature: The Berlin Clock + +Scenario: Midnight + When the time is 00:00:00 + Then the clock should look like + |Y| + |OOOO| + |OOOO| + |OOOOOOOOOOO| + |OOOO| + +Scenario: Middle of the afternoon +When the time is 13:17:01 +Then the clock should look like +| O | +| RROO | +| RRRO | +| YYROOOOOOOO | +| YYOO | + +Scenario: Just before midnight +When the time is 23:59:59 +Then the clock should look like +| O | +| RRRR | +| RRRO | +| YYRYYRYYRYY | +| YYYY | + +Scenario: Null time +When the invalid time is null +Then the clock should throw an error "Invalid time format" + +Scenario: Empty time value +When the time is "" +Then the clock should throw an error "Invalid time format" + +Scenario: Invalid time format (with dots instead of colons) +When the time is 23.34.34 +Then the clock should throw an error "Invalid time format" + +Scenario: Invalid hour value (greater than 24) +When the time is 25:30:45 +Then the clock should throw an error "Invalid time format" + +Scenario: Invalid minute value (greater than 59) +When the time is 12:60:30 +Then the clock should throw an error "Invalid time format" + +Scenario: Invalid second value (greater than 59) +When the time is 12:30:60 +Then the clock should throw an error "Invalid time format" + +Scenario: Invalid time format (non-numeric value) +When the time is "abc:def:ghi" +Then the clock should throw an error "Invalid time format" diff --git a/src/test/resources/stories/berlin-clock.story b/src/test/resources/stories/berlin-clock.story deleted file mode 100644 index 8d4d15d..0000000 --- a/src/test/resources/stories/berlin-clock.story +++ /dev/null @@ -1,76 +0,0 @@ -Story: The Berlin Clock - -Meta: -@scope interview - -Narrative: - As a clock enthusiast - I want to tell the time using the Berlin Clock - So that I can increase then number of ways that I can read the time - -Scenario: Midnight -When the time is 00:00:00 -Then the clock should look like -Y -OOOO -OOOO -OOOOOOOOOOO -OOOO - -Scenario: Middle of the afternoon -When the time is 13:17:01 -Then the clock should look like -O -RROO -RRRO -YYROOOOOOOO -YYOO - -Scenario: Just before midnight -When the time is 23:59:59 -Then the clock should look like -O -RRRR -RRRO -YYRYYRYYRYY -YYYY - -Scenario: Midnight -When the time is 24:00:00 -Then the clock should look like -Y -RRRR -RRRR -OOOOOOOOOOO -OOOO - -Scenario: Null time - When the time is null - Then the clock should throw an error "Invalid time format" - -Scenario: Empty time value - When the time is "" - Then the clock should throw an error "Invalid time format" - -Scenario: Invalid time format (with dots instead of colons) - When the time is 23.34.34 - Then the clock should throw an error "Invalid time format" - -Scenario: Invalid hour value (greater than 24) - When the time is 25:30:45 - Then the clock should throw an error "Invalid time format" - -Scenario: Invalid minute value (greater than 59) - When the time is 12:60:30 - Then the clock should throw an error "Invalid time format" - -Scenario: Invalid second value (greater than 59) - When the time is 12:30:60 - Then the clock should throw an error "Invalid time format" - -Scenario: Invalid time format (non-numeric value) - When the time is "abc:def:ghi" - Then the clock should throw an error "Invalid time format" - - -