diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml new file mode 100644 index 0000000..1180419 --- /dev/null +++ b/.github/workflows/java.yml @@ -0,0 +1,25 @@ +name: Build Java + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v5 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + - name: Build with Gradle + run: ./gradlew build diff --git a/README.md b/README.md index 87ffcbb..1a6a1a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/4ISSpVK4) **Java reflection** Ваша задача --- написать генератор экземпляров произвольных классов. diff --git a/annotation-processor/build.gradle.kts b/annotation-processor/build.gradle.kts new file mode 100644 index 0000000..aca3885 --- /dev/null +++ b/annotation-processor/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("java") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":core")) +} diff --git a/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java b/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java new file mode 100644 index 0000000..586da10 --- /dev/null +++ b/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java @@ -0,0 +1,95 @@ +package org.example.annotation.processor; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; + +import org.example.annotation.Generatable; + +@SupportedSourceVersion(SourceVersion.RELEASE_21) +@SupportedAnnotationTypes("org.example.annotation.Generatable") +public class GeneratableProcessor extends AbstractProcessor { + private boolean hasGenerated = false; + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (hasGenerated || roundEnv.processingOver()) { + return true; + } + + var annotatedElements = roundEnv.getElementsAnnotatedWith(Generatable.class); + + var typeElements = annotatedElements.stream() + .map(this::getTypeElement) + .filter(Objects::nonNull) + .toList(); + + if (!typeElements.isEmpty()) { + generateGeneratableRegistry(typeElements); + hasGenerated = true; + } + + return true; + } + + private TypeElement getTypeElement(Element element) { + return (element.getKind() == ElementKind.CLASS) ? (TypeElement) element : null; + } + + /** + * При процессинге аннотации {@link Generatable} "кэшируем" классы, которые её используют, + * для дальнейшего поиска при генерации производных классов, реализующих интерфейсы + */ + private void generateGeneratableRegistry(List typeElements) { + try { + JavaFileObject file = processingEnv.getFiler() + .createSourceFile("org.example.generator.GeneratableRegistry"); + + try (Writer writer = new BufferedWriter(file.openWriter())) { + writer.write("package org.example.generator;\n\n"); + writer.write("import java.util.List;\n\n"); + writer.write("// Auto-generated by GeneratableProcessor. Don't modify this code!\n"); + writer.write("public class GeneratableRegistry {\n"); + writer.write(" public static List> getAllGeneratableClasses() {\n"); + writer.write(" return List.of(\n"); + + for (int i = 0; i < typeElements.size(); i++) { + writer.write(" " + typeElements.get(i).getQualifiedName().toString() + ".class"); + if (i < typeElements.size() - 1) { + writer.write(",\n"); + } else { + writer.write("\n"); + } + } + + writer.write(" );\n"); + writer.write(" }\n"); + writer.write("}\n"); + } + + processingEnv.getMessager().printMessage( + Diagnostic.Kind.NOTE, + "Successfully generated GeneratableRegistry with " + typeElements.size() + " classes" + ); + } catch (IOException e) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Failed to generate registry: " + e.getMessage() + ); + } + } +} diff --git a/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..e53d0ec --- /dev/null +++ b/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.example.annotation.processor.GeneratableProcessor diff --git a/build.gradle.kts b/build.gradle.kts index b4738ae..0e87e53 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,9 @@ repositories { } dependencies { + implementation(project(":core")) + annotationProcessor(project(":annotation-processor")) + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..812f64a --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { +} diff --git a/core/src/main/java/org/example/annotation/Generatable.java b/core/src/main/java/org/example/annotation/Generatable.java new file mode 100644 index 0000000..948f020 --- /dev/null +++ b/core/src/main/java/org/example/annotation/Generatable.java @@ -0,0 +1,13 @@ +package org.example.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Generatable { +} diff --git a/settings.gradle.kts b/settings.gradle.kts index af5b8ff..b28bd35 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,3 @@ -rootProject.name = "reflection-template" \ No newline at end of file +rootProject.name = "reflection-template" +include("annotation-processor") +include("core") diff --git a/src/main/java/org/example/GenerateExample.java b/src/main/java/org/example/GenerateExample.java index 47679a9..c448574 100644 --- a/src/main/java/org/example/GenerateExample.java +++ b/src/main/java/org/example/GenerateExample.java @@ -1,12 +1,19 @@ package org.example; +import java.util.List; +import java.util.Random; + import org.example.classes.Example; import org.example.generator.Generator; +import org.example.generator.PrimitiveValueGenerator; +import org.example.generator.StringValueGenerator; public class GenerateExample { public static void main(String[] args) { - var gen = new Generator(); + var random = new Random(42); + var gen = new Generator(random, List.of(new PrimitiveValueGenerator(random), new StringValueGenerator(random))); + try { Object generated = gen.generateValueOfType(Example.class); System.out.println(generated); diff --git a/src/main/java/org/example/classes/BinaryTreeNode.java b/src/main/java/org/example/classes/BinaryTreeNode.java index 046ff56..431197d 100644 --- a/src/main/java/org/example/classes/BinaryTreeNode.java +++ b/src/main/java/org/example/classes/BinaryTreeNode.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class BinaryTreeNode { private Integer data; private BinaryTreeNode left; @@ -30,4 +33,13 @@ public void setLeft(BinaryTreeNode left) { public void setRight(BinaryTreeNode right) { this.right = right; } + + @Override + public String toString() { + return "BinaryTreeNode{" + + "data=" + data + + ", left=" + left + + ", right=" + right + + '}'; + } } diff --git a/src/main/java/org/example/classes/Cart.java b/src/main/java/org/example/classes/Cart.java index 965237d..1994482 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/main/java/org/example/classes/Cart.java @@ -2,6 +2,9 @@ import java.util.List; +import org.example.annotation.Generatable; + +@Generatable public class Cart { private List items; @@ -18,4 +21,11 @@ public void setItems(List items) { } // Конструктор, методы добавления и удаления товаров, геттеры и другие методы -} \ No newline at end of file + + @Override + public String toString() { + return "Cart{" + + "items=" + items + + '}'; + } +} diff --git a/src/main/java/org/example/classes/CartStorage.java b/src/main/java/org/example/classes/CartStorage.java new file mode 100644 index 0000000..a562232 --- /dev/null +++ b/src/main/java/org/example/classes/CartStorage.java @@ -0,0 +1,31 @@ +package org.example.classes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.example.annotation.Generatable; + +@Generatable +public class CartStorage { + private final Map> buyerCarts; + + public CartStorage(Map> buyerCarts) { + this.buyerCarts = buyerCarts; + } + + public Map> getBuyerCarts() { + return buyerCarts; + } + + public void addBuyerCart(String buyer, Cart cart) { + buyerCarts.computeIfAbsent(buyer, k -> new ArrayList<>()).add(cart); + } + + @Override + public String toString() { + return "CartStorage{" + + "buyerCarts=" + buyerCarts + + '}'; + } +} diff --git a/src/main/java/org/example/classes/Example.java b/src/main/java/org/example/classes/Example.java index eac9463..39d401f 100644 --- a/src/main/java/org/example/classes/Example.java +++ b/src/main/java/org/example/classes/Example.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Example { int i; @@ -9,6 +12,8 @@ public Example(int i) { @Override public String toString() { - return "Example(" + i + ")"; + return "Example{" + + "i=" + i + + '}'; } } diff --git a/src/main/java/org/example/classes/Product.java b/src/main/java/org/example/classes/Product.java index e7dcc89..e183bc9 100644 --- a/src/main/java/org/example/classes/Product.java +++ b/src/main/java/org/example/classes/Product.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Product { private String name; private double price; @@ -30,19 +33,11 @@ public void setPrice(double price) { this.price = price; } - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return super.equals(obj); - } - @Override public String toString() { - return super.toString(); + return "Product{" + + "name='" + name + '\'' + + ", price=" + price + + '}'; } - } diff --git a/src/main/java/org/example/classes/Rectangle.java b/src/main/java/org/example/classes/Rectangle.java index 90b0886..8346da6 100644 --- a/src/main/java/org/example/classes/Rectangle.java +++ b/src/main/java/org/example/classes/Rectangle.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Rectangle implements Shape { private double length; private double width; @@ -18,4 +21,12 @@ public double getArea() { public double getPerimeter() { return 2 * (length + width); } -} \ No newline at end of file + + @Override + public String toString() { + return "Rectangle{" + + "length=" + length + + ", width=" + width + + '}'; + } +} diff --git a/src/main/java/org/example/classes/Shape.java b/src/main/java/org/example/classes/Shape.java index c20a851..e105048 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/main/java/org/example/classes/Shape.java @@ -3,4 +3,4 @@ public interface Shape { double getArea(); double getPerimeter(); -} \ No newline at end of file +} diff --git a/src/main/java/org/example/classes/Triangle.java b/src/main/java/org/example/classes/Triangle.java index 011e96f..62e316c 100644 --- a/src/main/java/org/example/classes/Triangle.java +++ b/src/main/java/org/example/classes/Triangle.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Triangle implements Shape { private double sideA; private double sideB; @@ -21,4 +24,13 @@ public double getArea() { public double getPerimeter() { return sideA + sideB + sideC; } -} \ No newline at end of file + + @Override + public String toString() { + return "Triangle{" + + "sideA=" + sideA + + ", sideB=" + sideB + + ", sideC=" + sideC + + '}'; + } +} diff --git a/src/main/java/org/example/generator/AbstractValueGenerator.java b/src/main/java/org/example/generator/AbstractValueGenerator.java new file mode 100644 index 0000000..751e015 --- /dev/null +++ b/src/main/java/org/example/generator/AbstractValueGenerator.java @@ -0,0 +1,25 @@ +package org.example.generator; + +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +public abstract class AbstractValueGenerator implements ValueGenerator { + private final Random random; + + protected AbstractValueGenerator(Random random) { + this.random = random; + } + + @Override + public boolean supports(Class clazz) { + return getMappingFunction().containsKey(clazz); + } + + @Override + public Object generate(Class clazz) { + return getMappingFunction().get(clazz).apply(random); + } + + protected abstract Map, Function> getMappingFunction(); +} diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 9d86bfb..1b8980e 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,18 +1,202 @@ package org.example.generator; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.Set; +import java.util.function.Supplier; + +import org.example.annotation.Generatable; public class Generator { + public static final int MAX_COLLECTION_SIZE = 5; + public static final int MAX_RECURSION_DEPTH = 3; + private final Random random; + private final List valueGenerators; + + public Generator( + Random random, + List valueGenerators + ) { + this.random = random; + this.valueGenerators = valueGenerators; + } + + public Object generateValueOfType(Class clazz) { + try { + return generateValueOfTypeInternal(clazz, null, 0); + } catch (Exception e) { + throw new RuntimeException("Failed to generate value for type: " + clazz.getName(), e); + } + } + + private Object generateValueOfTypeInternal(Class clazz, Type genericType, int depth) throws Exception { + var valueGenerator = findValueGeneratorForClass(clazz); + if (valueGenerator != null) { + return valueGenerator.generate(clazz); + } + + if (depth > MAX_RECURSION_DEPTH) { + return null; + } + + if (Collection.class.isAssignableFrom(clazz)) { + return generateCollection(clazz, genericType, depth); + } + if (Map.class.isAssignableFrom(clazz)) { + return generateMap(genericType, depth); + } + + if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { + return generateInterfaceImplementation(clazz, depth); + } + + if (!clazz.isAnnotationPresent(Generatable.class)) { + throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Generatable"); + } + + Constructor constructor = selectConstructor(clazz); + Object[] params = generateParameters(constructor, depth + 1); + return constructor.newInstance(params); + } + + private ValueGenerator findValueGeneratorForClass(Class clazz) { + return valueGenerators.stream() + .filter(it -> it.supports(clazz)) + .findFirst() + .orElse(null); + } + + private Collection generateCollection(Class clazz, Type genericType, int depth) throws Exception { + if (List.class.isAssignableFrom(clazz)) { + return generateCollectionInternal(genericType, depth, ArrayList::new); + } else if (Set.class.isAssignableFrom(clazz)) { + return generateCollectionInternal(genericType, depth, HashSet::new); + } else { + return generateCollectionInternal(genericType, depth, ArrayDeque::new); + } + } + + private Collection generateCollectionInternal( + Type genericType, + int depth, + Supplier> collectionSupplier + ) throws Exception { + Class elementType = Object.class; + Type elementGenericType = null; + + if (genericType instanceof ParameterizedType paramType) { + Type[] typeArgs = paramType.getActualTypeArguments(); + if (typeArgs.length > 0) { + elementGenericType = typeArgs[0]; + elementType = getClassFromType(elementGenericType); + } + } + + int size = random.nextInt(MAX_COLLECTION_SIZE + 1); + var collection = collectionSupplier.get(); + for (int i = 0; i < size; i++) { + Object element = generateValueOfTypeInternal(elementType, elementGenericType, depth); + collection.add(element); + } + return collection; + } - public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { + private Map generateMap(Type genericType, int depth) throws Exception { + Class keyType = Object.class; + Type keyGenericType = null; + Class valueType = Object.class; + Type valueGenericType = null; + + if (genericType instanceof ParameterizedType paramType) { + Type[] typeArgs = paramType.getActualTypeArguments(); + if (typeArgs.length > 0) { + keyGenericType = typeArgs[0]; + keyType = getClassFromType(keyGenericType); + } + if (typeArgs.length > 1) { + valueGenericType = typeArgs[1]; + valueType = getClassFromType(valueGenericType); + } + } + + int size = random.nextInt(MAX_COLLECTION_SIZE + 1); + Map map = new HashMap<>(); + for (int i = 0; i < size; i++) { + Object key = generateValueOfTypeInternal(keyType, keyGenericType, depth); + Object value = generateValueOfTypeInternal(valueType, valueGenericType, depth); + map.put(key, value); + } + return map; + } + + private Class getClassFromType(Type type) { + if (type instanceof Class cls) { + return cls; + } else if (type instanceof ParameterizedType paramType) { + return (Class) paramType.getRawType(); + } + return Object.class; + } + + private Object generateInterfaceImplementation(Class interfaceClass, int depth) throws Exception { + List> implementations = findGeneratableImplementations(interfaceClass); + + if (implementations.isEmpty()) { + throw new IllegalArgumentException( + "No @Generatable implementations found for interface/abstract class: " + interfaceClass.getName() + ); + } + + Class selectedClass = implementations.get(random.nextInt(implementations.size())); + return generateValueOfTypeInternal(selectedClass, null, depth); + } + + private List> findGeneratableImplementations(Class interfaceClass) { + return GeneratableRegistry.getAllGeneratableClasses().stream() + .filter(it -> isGeneratableClass(it, interfaceClass)) + .toList(); + } + + private boolean isGeneratableClass(Class clazz, Class interfaceClass) { + return interfaceClass.isAssignableFrom(clazz) && + !clazz.isInterface() && + !Modifier.isAbstract(clazz.getModifiers()); + } + + private Constructor selectConstructor(Class clazz) { Constructor[] constructors = clazz.getDeclaredConstructors(); - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); + if (constructors.length == 0) { + throw new IllegalArgumentException("No constructors found for class: " + clazz.getName()); + } + + int selectedIndex = random.nextInt(constructors.length); + + return constructors[selectedIndex]; } + private Object[] generateParameters(Constructor constructor, int depth) throws Exception { + Parameter[] parameters = constructor.getParameters(); + Object[] result = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + Type genericType = parameters[i].getParameterizedType(); + Class paramType = parameters[i].getType(); + result[i] = generateValueOfTypeInternal(paramType, genericType, depth); + } + + return result; + } } diff --git a/src/main/java/org/example/generator/PrimitiveValueGenerator.java b/src/main/java/org/example/generator/PrimitiveValueGenerator.java new file mode 100644 index 0000000..e95d201 --- /dev/null +++ b/src/main/java/org/example/generator/PrimitiveValueGenerator.java @@ -0,0 +1,43 @@ +package org.example.generator; + +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +import static java.util.Map.entry; + +public class PrimitiveValueGenerator extends AbstractValueGenerator { + private static final Integer ALPHABET_SIZE = 26; + + private static final Map, Function> MAPPING_FUNCTION = Map.ofEntries( + entry(int.class, Random::nextInt), + entry(Integer.class, Random::nextInt), + entry(long.class, Random::nextLong), + entry(Long.class, Random::nextLong), + entry(double.class, (random) -> random.nextDouble() * 1000), + entry(Double.class, (random) -> random.nextDouble() * 1000), + entry(float.class, (random) -> random.nextFloat() * 1000), + entry(Float.class, (random) -> random.nextFloat() * 1000), + entry(short.class, (random) -> (short) random.nextInt(Short.MAX_VALUE)), + entry(Short.class, (random) -> (short) random.nextInt(Short.MAX_VALUE)), + entry(byte.class, (random) -> (byte) random.nextInt(Byte.MAX_VALUE)), + entry(Byte.class, (random) -> (byte) random.nextInt(Byte.MAX_VALUE)), + entry(boolean.class, Random::nextBoolean), + entry(Boolean.class, Random::nextBoolean), + entry(char.class, PrimitiveValueGenerator::nextChar), + entry(Character.class, PrimitiveValueGenerator::nextChar) + ); + + public PrimitiveValueGenerator(Random random) { + super(random); + } + + @Override + protected Map, Function> getMappingFunction() { + return MAPPING_FUNCTION; + } + + private static char nextChar(Random random) { + return (char) ('a' + random.nextInt(ALPHABET_SIZE)); + } +} diff --git a/src/main/java/org/example/generator/StringValueGenerator.java b/src/main/java/org/example/generator/StringValueGenerator.java new file mode 100644 index 0000000..3a5b954 --- /dev/null +++ b/src/main/java/org/example/generator/StringValueGenerator.java @@ -0,0 +1,29 @@ +package org.example.generator; + +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +public class StringValueGenerator extends AbstractValueGenerator { + private static final Integer MAX_STRING_LENGTH = 50; + + private static final Map, Function> MAPPING_FUNCTION = Map.of( + String.class, (random) -> StringValueGenerator.nextString(random, random.nextInt(MAX_STRING_LENGTH)) + ); + + public StringValueGenerator(Random random) { + super(random); + } + + @Override + protected Map, Function> getMappingFunction() { + return MAPPING_FUNCTION; + } + + private static String nextString(Random random, int length) { + return random.ints('a', 'z') + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/src/main/java/org/example/generator/ValueGenerator.java b/src/main/java/org/example/generator/ValueGenerator.java new file mode 100644 index 0000000..21da2b5 --- /dev/null +++ b/src/main/java/org/example/generator/ValueGenerator.java @@ -0,0 +1,6 @@ +package org.example.generator; + +public interface ValueGenerator { + boolean supports(Class clazz); + Object generate(Class clazz); +} diff --git a/src/test/java/org/example/generator/GeneratorTest.java b/src/test/java/org/example/generator/GeneratorTest.java new file mode 100644 index 0000000..b83ce36 --- /dev/null +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -0,0 +1,205 @@ +package org.example.generator; + +import java.util.List; +import java.util.Random; + +import org.example.classes.BinaryTreeNode; +import org.example.classes.Cart; +import org.example.classes.CartStorage; +import org.example.classes.Example; +import org.example.classes.Product; +import org.example.classes.Rectangle; +import org.example.classes.Shape; +import org.example.classes.Triangle; +import org.junit.jupiter.api.Test; + +import static org.example.generator.Generator.MAX_COLLECTION_SIZE; +import static org.example.generator.Generator.MAX_RECURSION_DEPTH; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GeneratorTest { + private final Random random = new Random(42); + private final Generator generator = new Generator(random, List.of( + new PrimitiveValueGenerator(random), + new StringValueGenerator(random) + )); + + @Test + void generateExample() { + Object result = generator.generateValueOfType(Example.class); + + assertNotNull(result); + assertInstanceOf(Example.class, result); + + Example example = (Example) result; + System.out.println("Generated Example: " + example); + } + + @Test + void generateProduct() { + Object result = generator.generateValueOfType(Product.class); + + assertNotNull(result); + assertInstanceOf(Product.class, result); + + Product product = (Product) result; + assertNotNull(product.getName(), "Product name should not be null"); + + System.out.println("Generated Product: " + product); + } + + @Test + void generateRectangle() { + Object result = generator.generateValueOfType(Rectangle.class); + + assertNotNull(result); + assertInstanceOf(Rectangle.class, result); + + Rectangle rectangle = (Rectangle) result; + + System.out.println("Generated Rectangle: " + rectangle); + } + + @Test + void generateTriangle() { + Object result = generator.generateValueOfType(Triangle.class); + + assertNotNull(result); + assertInstanceOf(Triangle.class, result); + + Triangle triangle = (Triangle) result; + System.out.println("Generated Triangle: " + triangle); + } + + @Test + void generateCart() { + Object result = generator.generateValueOfType(Cart.class); + + assertNotNull(result); + assertInstanceOf(Cart.class, result); + + Cart cart = (Cart) result; + assertNotNull(cart.getItems(), "Cart items should not be null"); + assertTrue( + cart.getItems().size() <= MAX_COLLECTION_SIZE, + "Cart should have at most" + MAX_COLLECTION_SIZE + " items" + ); + + cart.getItems().forEach(item -> { + assertInstanceOf(Product.class, item, "Cart item should be Product"); + assertNotNull(item.getName()); + }); + + System.out.println("Generated Cart: " + cart); + } + + @Test + void generateCartStorage() { + Object result = generator.generateValueOfType(CartStorage.class); + + assertNotNull(result); + assertInstanceOf(CartStorage.class, result); + + CartStorage cartStorage = (CartStorage) result; + assertNotNull(cartStorage.getBuyerCarts(), "Buyer carts map should not be null"); + assertTrue( + cartStorage.getBuyerCarts().size() <= MAX_COLLECTION_SIZE, + "CartStorage should have at most " + MAX_COLLECTION_SIZE + " buyers" + ); + + cartStorage.getBuyerCarts().forEach((buyer, carts) -> { + assertNotNull(buyer, "Buyer name (key) should not be null"); + assertInstanceOf(String.class, buyer, "Key should be String"); + + assertNotNull(carts, "Cart list should not be null"); + assertInstanceOf(List.class, carts, "Value should be List"); + assertTrue(carts.size() <= MAX_COLLECTION_SIZE, "Cart list should be limited"); + + carts.forEach(cart -> { + assertInstanceOf(Cart.class, cart, "Element should be Cart"); + assertNotNull(cart.getItems(), "Cart items should not be null"); + assertTrue(cart.getItems().size() <= MAX_COLLECTION_SIZE, "Cart items should be limited"); + + cart.getItems().forEach(product -> { + assertInstanceOf(Product.class, product); + assertNotNull(product.getName()); + }); + }); + }); + + System.out.println("Generated CartStorage: " + cartStorage); + } + + @Test + void generateShapeImplementation() { + Object result = generator.generateValueOfType(Shape.class); + + assertNotNull(result); + assertInstanceOf(Shape.class, result); + assertTrue(result instanceof Rectangle || result instanceof Triangle, + "Shape should be either Rectangle or Triangle"); + + Shape shape = (Shape) result; + + System.out.println("Generated Shape: " + shape.getClass().getSimpleName() + " = " + shape); + } + + @Test + void generateBinaryTreeNode() { + Object result = generator.generateValueOfType(BinaryTreeNode.class); + + assertNotNull(result); + assertInstanceOf(BinaryTreeNode.class, result); + + BinaryTreeNode tree = (BinaryTreeNode) result; + assertNotNull(tree.getData(), "Root data should not be null"); + + int depth = calculateTreeDepth(tree); + assertTrue(depth <= MAX_RECURSION_DEPTH + 1, "Tree depth should be limited"); + + System.out.println("Generated BinaryTreeNode with depth " + depth + ":"); + printTree(tree, "", true); + } + + @Test + void generateNotGeneratableThrows() { + class NotGeneratable { + public NotGeneratable(int x) {} + } + + assertThrows(RuntimeException.class, () -> + generator.generateValueOfType(NotGeneratable.class) + ); + } + + private int calculateTreeDepth(BinaryTreeNode node) { + if (node == null) { + return 0; + } + return Math.max( + calculateTreeDepth(node.getLeft()), + calculateTreeDepth(node.getRight()) + ) + 1; + } + + private void printTree(BinaryTreeNode node, String prefix, boolean isTail) { + if (node == null) { + return; + } + + System.out.println(prefix + (isTail ? "└── " : "├── ") + node.getData()); + + if (node.getLeft() != null || node.getRight() != null) { + if (node.getLeft() != null) { + printTree(node.getLeft(), prefix + (isTail ? " " : "│ "), node.getRight() == null); + } + if (node.getRight() != null) { + printTree(node.getRight(), prefix + (isTail ? " " : "│ "), true); + } + } + } +} +