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 + } + +}