diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
deleted file mode 100644
index d505847..0000000
--- a/.github/workflows/maven.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
-
-name: Java CI with Maven
-
-on:
- push:
- branches: [ "master" ]
- pull_request:
- branches: [ "master" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up JDK 11
- uses: actions/setup-java@v3
- with:
- java-version: '17'
- distribution: 'temurin'
- cache: maven
- - name: Build with Maven
- run: mvn -B package --file pom.xml
diff --git a/.woodpecker/maven.yml b/.woodpecker/maven.yml
new file mode 100644
index 0000000..877f1e3
--- /dev/null
+++ b/.woodpecker/maven.yml
@@ -0,0 +1,5 @@
+steps:
+ - name: test
+ image: maven:3.9.6-eclipse-temurin-17-alpine
+ commands:
+ - mvn -B verify
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..3f1e4dc
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+ eu.ztsh.training
+ hackerrank
+ 1.0-SNAPSHOT
+
+
+ 17
+ 17
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.9.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.23.1
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/eu/ztsh/training/hackerrank/EnvironmentTest.java b/src/test/java/eu/ztsh/training/hackerrank/EnvironmentTest.java
new file mode 100644
index 0000000..fda2bda
--- /dev/null
+++ b/src/test/java/eu/ztsh/training/hackerrank/EnvironmentTest.java
@@ -0,0 +1,126 @@
+package eu.ztsh.training.hackerrank;
+
+import java.util.List;
+import java.util.Scanner;
+
+import eu.ztsh.training.hackerrank.SolutionClassDescription.FieldModifier;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EnvironmentTest {
+
+ abstract static class HackerRankEnvironmentTest extends HackerRankTest {
+
+ @Test
+ public void passTwoLinesTest() {
+ var input = List.of(
+ "Line 1",
+ "Line 2"
+ );
+ var result = invoke(input);
+ assertThat(result).containsExactlyElementsOf(input);
+ }
+
+ @Test
+ public void multiInvokeTest() {
+ var input = List.of(
+ "Line 1",
+ "Line 2"
+ );
+ invoke(input);
+ invoke(input);
+ var result = invoke(input);
+ assertThat(result).containsExactlyElementsOf(input);
+ }
+
+ @Test
+ public void passTwoAnotherLinesTest() {
+ var input = List.of(
+ "Line 3",
+ "Line 4"
+ );
+ var result = invoke(input);
+ assertThat(result).containsExactlyElementsOf(input);
+ }
+
+ }
+
+ @Nested
+ @DisplayName("Test with Scanner created as private static field")
+ class EnvironmentPrivateStaticTest extends HackerRankEnvironmentTest {
+
+ @Override
+ protected SolutionClassDescription getSolutionClassDescription() {
+ return new SolutionClassDescription(SampleSolutionWithPrivateStaticScanner.class,
+ "scan",
+ new FieldModifier[]{FieldModifier.PRIVATE, FieldModifier.STATIC});
+ }
+
+ }
+
+ @Disabled("Disabled: run with --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED")
+ @Nested
+ @DisplayName("Test with Scanner created as private static final field")
+ class EnvironmentPrivateStaticFinalTest extends HackerRankEnvironmentTest {
+
+ @Override
+ protected SolutionClassDescription getSolutionClassDescription() {
+ return new SolutionClassDescription(SampleSolutionWithPrivateStaticScanner.class,
+ "scan",
+ FieldModifier.values());
+ }
+
+ }
+
+ @Nested
+ @DisplayName("Test with Scanner created in main(String[]) method")
+ class EnvironmentInlineTest extends HackerRankEnvironmentTest {
+
+ @Override
+ protected SolutionClassDescription getSolutionClassDescription() {
+ return new SolutionClassDescription(SampleSolutionWithInlineScanner.class);
+ }
+
+ }
+
+}
+
+class SampleSolutionWithPrivateStaticScanner {
+
+ public static void main(String... args) {
+ while (scan.hasNext()) {
+ System.out.println(scan.nextLine());
+ }
+ }
+
+ @SuppressWarnings("FieldMayBeFinal")
+ private static Scanner scan = new Scanner(System.in);
+
+}
+
+class SampleSolutionWithPrivateStaticFinalScanner {
+
+ public static void main(String... args) {
+ while (scan.hasNext()) {
+ System.out.println(scan.nextLine());
+ }
+ }
+
+ private static final Scanner scan = new Scanner(System.in);
+
+}
+
+class SampleSolutionWithInlineScanner {
+
+ public static void main(String... args) {
+ var scan = new Scanner(System.in);
+ while (scan.hasNext()) {
+ System.out.println(scan.nextLine());
+ }
+ }
+
+}
diff --git a/src/test/java/eu/ztsh/training/hackerrank/HackerRankTest.java b/src/test/java/eu/ztsh/training/hackerrank/HackerRankTest.java
new file mode 100644
index 0000000..106f212
--- /dev/null
+++ b/src/test/java/eu/ztsh/training/hackerrank/HackerRankTest.java
@@ -0,0 +1,73 @@
+package eu.ztsh.training.hackerrank;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+/**
+ * Common class with all the necessary logic
+ */
+public abstract class HackerRankTest {
+
+ @BeforeAll
+ public static void setUpStreams() {
+ // Redirect stdout & stderr to own streams
+ System.setOut(new PrintStream(outContent));
+ System.setErr(new PrintStream(errContent));
+ }
+
+ @AfterAll
+ public static void restoreStreams() {
+ System.setOut(originalOut);
+ System.setErr(originalErr);
+ System.setIn(originalIn);
+ }
+
+ protected List invoke(List input) {
+ try {
+ // reset our stdout as check is based on it
+ outContent.reset();
+ // write content to args of Solution#main
+ writeLines(input);
+ // check if scanner has other name than "scanner"
+ if (getSolutionClassDescription().fieldName() != null) {
+ ReflectionHelper.reloadScanner(getSolutionClassDescription());
+ }
+ // run Solution#main
+ getSolutionClassDescription().targetClass()
+ .getMethod("main", String[].class)
+ .invoke(null, (Object) new String[]{});
+ // return intercepted output
+ return readLines();
+ } catch (final NoSuchMethodException | InvocationTargetException | IllegalAccessException |
+ NoSuchFieldException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ protected abstract SolutionClassDescription getSolutionClassDescription();
+
+ private void writeLines(List lines) {
+ // https://stackoverflow.com/a/31635737
+ System.setIn(new ByteArrayInputStream((String.join(System.lineSeparator(), lines) + System.lineSeparator()).getBytes()));
+ }
+
+ private List readLines() {
+ // https://stackoverflow.com/a/1119559
+ return List.copyOf(Arrays.stream(outContent.toString().split(System.lineSeparator())).toList());
+ }
+
+ private final static ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final static ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ private final static PrintStream originalOut = System.out;
+ private final static PrintStream originalErr = System.err;
+ private final static InputStream originalIn = System.in;
+
+}
diff --git a/src/test/java/eu/ztsh/training/hackerrank/ReflectionHelper.java b/src/test/java/eu/ztsh/training/hackerrank/ReflectionHelper.java
new file mode 100644
index 0000000..d00b23e
--- /dev/null
+++ b/src/test/java/eu/ztsh/training/hackerrank/ReflectionHelper.java
@@ -0,0 +1,52 @@
+package eu.ztsh.training.hackerrank;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Scanner;
+
+import eu.ztsh.training.hackerrank.SolutionClassDescription.FieldModifier;
+
+/**
+ * Util that helps with scanner declared outside Solution#main
+ */
+public class ReflectionHelper {
+
+ // https://stackoverflow.com/a/56043252
+ static {
+ try {
+ var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
+ MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
+ CAN_MODIFY_FINALS = true;
+ } catch (IllegalAccessException | NoSuchFieldException ex) {
+ CAN_MODIFY_FINALS = false;
+ }
+ }
+
+ /**
+ * Modify scanner field to substitute it with custom readable one
+ *
+ * @param description Solution class parameters
+ * @throws NoSuchFieldException when there description.fieldName points to incorrect name
+ * @throws IllegalAccessException on scanner substitution error
+ */
+ static void reloadScanner(SolutionClassDescription description)
+ throws NoSuchFieldException, IllegalAccessException {
+ // https://stackoverflow.com/a/3301720
+ var scannerField = description.hasModifier(FieldModifier.STATIC)
+ ? description.targetClass().getDeclaredField(description.fieldName())
+ : description.targetClass().getField(description.fieldName());
+ if (description.hasModifier(FieldModifier.PRIVATE)) {
+ scannerField.setAccessible(true);
+ }
+ if (description.hasModifier(FieldModifier.FINAL) && CAN_MODIFY_FINALS) {
+ MODIFIERS.set(scannerField, scannerField.getModifiers() & ~Modifier.FINAL);
+ }
+ scannerField.set(null, new Scanner(System.in));
+ }
+
+ private static VarHandle MODIFIERS;
+ private static boolean CAN_MODIFY_FINALS;
+
+}
diff --git a/src/test/java/eu/ztsh/training/hackerrank/SolutionClassDescription.java b/src/test/java/eu/ztsh/training/hackerrank/SolutionClassDescription.java
new file mode 100644
index 0000000..2ac51dd
--- /dev/null
+++ b/src/test/java/eu/ztsh/training/hackerrank/SolutionClassDescription.java
@@ -0,0 +1,26 @@
+package eu.ztsh.training.hackerrank;
+
+import java.util.Arrays;
+
+/**
+ * Solution params definition
+ *
+ * @param targetClass Solution
+ * @param fieldName scanner field name
+ * @param modifiers scanner field modifiers
+ */
+public record SolutionClassDescription(Class> targetClass, String fieldName, FieldModifier[] modifiers) {
+
+ SolutionClassDescription(Class> targetClass) {
+ this(targetClass, null, null);
+ }
+
+ public boolean hasModifier(FieldModifier modifier) {
+ return Arrays.asList(modifiers).contains(modifier);
+ }
+
+ public enum FieldModifier {
+ PRIVATE, FINAL, STATIC
+ }
+
+}