Merge pull request 'dev' (#1) from dev into master

Reviewed-on: https://hattori.ztsh.eu/stawros/hackerrank/pulls/1
This commit is contained in:
Piotr Dec 2024-03-15 22:02:24 +01:00
commit 05620d9823
7 changed files with 313 additions and 26 deletions

View file

@ -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

5
.woodpecker/maven.yml Normal file
View file

@ -0,0 +1,5 @@
steps:
- name: test
image: maven:3.9.6-eclipse-temurin-17-alpine
commands:
- mvn -B verify

31
pom.xml Normal file
View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eu.ztsh.training</groupId>
<artifactId>hackerrank</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
</dependency>
</dependencies>
</project>

View file

@ -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());
}
}
}

View file

@ -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<String> invoke(List<String> 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<String> lines) {
// https://stackoverflow.com/a/31635737
System.setIn(new ByteArrayInputStream((String.join(System.lineSeparator(), lines) + System.lineSeparator()).getBytes()));
}
private List<String> 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;
}

View file

@ -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;
}

View file

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