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/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 efba2e7..943addd 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,39 @@ 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' + // 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/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..5f690e4 --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java @@ -0,0 +1,78 @@ +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 + */ +public class BerlinClock implements TimeConverter { + @Override + public String convertTime(String time) { + if (Objects.isNull(time)) { + throw new IllegalArgumentException("Invalid time format"); + } + + LocalTime localTime; + try { + 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 + * @param seconds + * @return + */ + protected String getLampOnOff(int seconds) { + // 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; + + // 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; + + // 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()); + + // Using Stream API to generate the second row of lamps (bottom minutes) + String bottomMinutesRow = getLampRowStream(4, numberBottomMinutesLamps, LampSymbol.YELLOW) + .collect(Collectors.joining()); + + return topMinutesRow + "\n" + bottomMinutesRow; + } + + 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) { + // 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/LampSymbol.java b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java new file mode 100644 index 0000000..38dcbd3 --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java @@ -0,0 +1,27 @@ +package com.ubs.opsit.interviews; + +/** + * @author Pankaj + */ +public enum LampSymbol { + 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 0310f71..0000000 --- a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ubs.opsit.interviews; - -import org.jbehave.core.annotations.Then; -import org.jbehave.core.annotations.When; -import org.junit.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 - * edit this class to complete the exercise, this is your definition of done. - */ -public class BerlinClockFixture { - - private TimeConverter berlinClock; - private String theTime; - - @Test - public void berlinClockAcceptanceTests() throws Exception { - aBehaviouralTestRunner() - .usingStepsFrom(this) - .withStory("berlin-clock.story") - .run(); - } - - @When("the time is $time") - public void whenTheTimeIs(String time) { - theTime = time; - } - - @Then("the clock should look like $") - public void thenTheClockShouldLookLike(String theExpectedBerlinClockOutput) { - assertThat(berlinClock.convertTime(theTime)).isEqualTo(theExpectedBerlinClockOutput); - } -} 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 c5abeba..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.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; - -/** - * 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() throws Exception { - 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() { - assertThat(stepsFactory).isNotNull(); - 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) { - assertThat(stepsFactory).isNull(); - 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 e74713a..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.WildcardFileFilter; -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) { - WildcardFileFilter regexFileFilter = new WildcardFileFilter(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/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 deleted file mode 100644 index 7d64304..0000000 --- a/src/test/resources/stories/berlin-clock.story +++ /dev/null @@ -1,48 +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 - - -