Skip to content

Commit

Permalink
Add fmf generator to allow users to generate fmf files from test docs (
Browse files Browse the repository at this point in the history
…#21)

Signed-off-by: Jakub Stejskal <[email protected]>
  • Loading branch information
Frawless authored Feb 14, 2024
1 parent ee6481e commit 0957563
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 98 deletions.
7 changes: 7 additions & 0 deletions test-docs-generator-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<junit.jupiter.version>5.10.2</junit.jupiter.version>
<junit.platform.version>1.10.2</junit.platform.version>
<maven.surefire.version>3.1.2</maven.surefire.version>
<snakeyaml.version>2.2</snakeyaml.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -131,6 +132,12 @@
<version>${maven-project.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package io.skodjob;

import io.skodjob.common.Utils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.LifecyclePhase;
Expand All @@ -20,9 +21,6 @@
import java.util.Locale;
import java.util.Map;

import static io.skodjob.DocGenerator.generate;
import static io.skodjob.DocGenerator.getTestClassesWithTheirPath;

/**
* DocGeneratorMojo class for Maven plugin handling
*/
Expand All @@ -45,6 +43,12 @@ public class DocGeneratorMojo extends AbstractMojo {
@Parameter(property = "generatePath", defaultValue = "./test-docs", required = true, readonly = true)
String generatePath;

/**
* Option for generating fmf
*/
@Parameter(property = "generateFmf", defaultValue = "false", readonly = true)
boolean generateFmf;

/**
* Pointer to Maven project
* Defaults to current project
Expand Down Expand Up @@ -91,12 +95,18 @@ public void execute() {
getLog().debug(url.getFile());
}

Map<String, String> classes = getTestClassesWithTheirPath(filePath, generatePath);
Map<String, String> classes = Utils.getTestClassesWithTheirPath(filePath, generatePath);

for (Map.Entry<String, String> entry : classes.entrySet()) {
try {
Class<?> testClass = classRealm.loadClass(entry.getValue());
generate(testClass, entry.getKey() + ".md");
MdGenerator.generate(testClass, entry.getKey() + ".md");
if (generateFmf) {
FmfGenerator.generate(testClass, entry.getKey() + ".fmf");
} else {
getLog().info("Skipping fmf generation");
}

} catch (ClassNotFoundException | IOException ex) {
getLog().warn(String.format("Cannot load %s", entry.getValue()));
getLog().error(ex);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright Skodjob authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.skodjob;

import io.skodjob.annotations.Step;
import io.skodjob.annotations.TestDoc;
import io.skodjob.annotations.TestTag;
import io.skodjob.annotations.UseCase;
import io.skodjob.common.Utils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;


/**
* The FmfGenerator generates FMF files for each documented test-case inside the test-class
* For each test-class is created separate FMF file - with the same name as the test-class
* All needed information is parsed from the particular @TestDoc annotation.
*/
public class FmfGenerator {

/**
* Generates test documentation in fmf format.
* Lists all methods (test-cases) annotated by {@link TestDoc} inside the {@param testClass}, creates
* parent folders (if needed), new FMF file for the class, and after that generates test-suite documentation using
* {@link #generateDocumentationForTestCases(PrintWriter, List)}, all written inside the newly created Markdown file.
*
* @param testClass for which the FMF file is created and test-cases are documented
* @param classFilePath path of the FMF file
* @throws IOException during file creation
*/
public static void generate(Class<?> testClass, String classFilePath) throws IOException {
List<Method> methods = Arrays.stream(testClass.getDeclaredMethods())
.filter(method -> method.getAnnotation(TestDoc.class) != null)
.toList();

if (!methods.isEmpty()) {
PrintWriter printWriter = Utils.createFilesForTestClass(classFilePath);

generateDocumentationForTestCases(printWriter, methods);

printWriter.close();
}
}

/**
* Generates documentation records for each test-cases (test-methods) from {@param methods}
* @param writer file writer
* @param methods containing {@link TestDoc} annotation
*/
private static void generateDocumentationForTestCases(PrintWriter writer, List<Method> methods) {
if (!methods.isEmpty()) {
methods.forEach(method -> {
TestDoc testDoc = method.getAnnotation(TestDoc.class);
if (testDoc != null) {
createTestRecord(writer, testDoc, method.getName());
}
});
}
}

/**
* Creates a single record of a test-case (test-method) inside the test-class
* The record contains: name of the test, description, steps, tags, and use-cases obtained from the
* {@param testDoc}.
*
* @param write file writer
* @param testDoc annotation containing all @TestDoc objects, from which is the record generated
* @param methodName name of the test-case containing {@param testDoc}
*/
public static void createTestRecord(PrintWriter write, TestDoc testDoc, String methodName) {

Map<String, Object> testCaseData = new HashMap<>();
testCaseData.put("summary", methodName);
// Make description multiline out of the box
String description = String.format("%s\n", testDoc.description().value());
testCaseData.put("description", description);
if (!Objects.equals(testDoc.contact().name(), "")) {
testCaseData.put("contact", String.format("%s <%s>", testDoc.contact().name(), testDoc.contact().email()));
}
if (testDoc.tags().length > 0) {
testCaseData.put("tags", createTags(testDoc.tags()));
}
if (testDoc.useCases().length > 0) {
testCaseData.put("usecases", createUseCases(testDoc.useCases()));
}
if (testDoc.steps().length > 0) {
testCaseData.put("steps", createListOfSteps(testDoc.steps()));
}

Map<String, Object> yamlData = new HashMap<>();
String testCaseKey = String.format("/%s", methodName);
yamlData.put(testCaseKey, testCaseData);

// Serialize the data to YAML
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);

yaml.dump(yamlData, write);
write.println();
}

/**
* For the provided list of steps creates table with following columns:
*
* @param steps list of steps of the test-case
* @return List of maps that represents all steps of teh test-case
*/
private static List<Map<String, String>> createListOfSteps(Step[] steps) {

List<Map<String, String>> listOfSteps = new ArrayList<>();

for (Step step : steps) {
Map<String, String> stepMap = new HashMap<>();
stepMap.put("step", step.value());
stepMap.put("result", step.expected());
listOfSteps.add(stepMap);
}

return listOfSteps;
}

/**
* Creates list of use-cases for the particular test-case
* @param usecases list of usecases from the {@link TestDoc} annotation
* @return list of usecases in {@link List<String>}
*/
private static List<String> createUseCases(UseCase[] usecases) {
List<String> fmfUsecases = new ArrayList<>();
Arrays.stream(usecases).forEach(usecase -> fmfUsecases.add(usecase.id()));

return fmfUsecases;
}

/**
* Creates list of tags for the particular test-case
* @param testTags list of tags from the {@link TestDoc} annotation
* @return list of tags in {@link List<String>}
*/
private static List<String> createTags(TestTag[] testTags) {
List<String> fmfTags = new ArrayList<>();
Arrays.stream(testTags).forEach(testTag -> fmfTags.add(testTag.value()));

return fmfTags;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,27 @@
import io.skodjob.annotations.TestDoc;
import io.skodjob.annotations.TestTag;
import io.skodjob.annotations.UseCase;
import io.skodjob.common.Utils;
import io.skodjob.markdown.Header;
import io.skodjob.markdown.Line;
import io.skodjob.markdown.Table;
import io.skodjob.markdown.TextList;
import io.skodjob.markdown.TextStyle;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;

/**
* Main Test Docs Generator class of the module
* The DocGenerator generates Markdown files for each documented test-case inside the test-class
* The MdGenerator generates Markdown files for each documented test-case inside the test-class
* For each test-class is created separate Markdown file - with the same name as the test-class
* All needed information is parsed from the particular @TestDoc annotation.
*/
public class DocGenerator {
private static final Pattern REMOVE_BEFORE_PACKAGE = Pattern.compile(".*java\\/");
public class MdGenerator {

/**
* Method that generates test documentation of the specified test-class.
Expand All @@ -59,7 +50,7 @@ public static void generate(Class<?> testClass, String classFilePath) throws IOE
.toList();

if (suiteDoc != null || !methods.isEmpty()) {
PrintWriter printWriter = createFilesForTestClass(classFilePath);
PrintWriter printWriter = Utils.createFilesForTestClass(classFilePath);

// creating first level header for the test-suite
printWriter.println(Header.firstLevelHeader(testClass.getSimpleName()));
Expand All @@ -71,27 +62,6 @@ public static void generate(Class<?> testClass, String classFilePath) throws IOE
}
}

/**
* Creates needed files and folders for the particular test-suite (test-class)
* @param classFilePath path where the test-suite (test-class) is present
* @return file writer
* @throws IOException during file creation
*/
private static PrintWriter createFilesForTestClass(String classFilePath) throws IOException {
String fileName = classFilePath.substring(classFilePath.lastIndexOf('/') + 1);
String parentPath = classFilePath.replace(fileName, "");

final File parent = new File(parentPath);
if (!parent.mkdirs()) {
System.err.println("Could not create parent directories ");
}
final File classFile = new File(parent, fileName);
classFile.createNewFile();

FileWriter write = new FileWriter(classFile);
return new PrintWriter(write);
}

/**
* Generates documentation for the test-suite (test-class) if {@link SuiteDoc} is present
* @param writer file writer
Expand Down Expand Up @@ -245,60 +215,4 @@ private static List<String> createTags(TestTag[] testTags) {

return usesText;
}

/**
* On specified {@param filePath} lists all classes and returns them in Map, where key is target
* path, where the Markdown file will be created, and value is package with class name available
* on classpath
* Also excludes files that should not be considered for documentation (currently just "AbstractST")
* @param filePath path where are all test-classes present
* @param generatePath "prefix" path where the documentation should be generated into
* @return Map with test-classes info from the {@param filePath}
*/
static Map<String, String> getTestClassesWithTheirPath(String filePath, String generatePath) {
Map<String, String> classes = new HashMap<>();

try {
Files.list(Paths.get(filePath))
.filter(file -> !file.getFileName().toString().contains("AbstractST"))
.forEach(path -> classes.putAll(getClassesForPackage(classes, path, generatePath)));
} catch (IOException exception) {
throw new RuntimeException(exception);
}

return classes;
}

/**
* Updates Map ({@param classes}) with info about classes inside {@param packagePath}.
* It goes through all files inside the {@param packagePath} and does two things:
* - in case that file is directory (another package), it recursively calls itself and adds all info needed for all files
* inside the directory
* - otherwise adds key/value record in the map
* - key -> path in which the particular `.md` file will be generated, typically {@param generatePath}/{@code classPackagePath}
* - f.e. -> ./test-docs/path/to/my/package/TestClassST
* - value -> path for the particular test class -> in package format, available on classpath
* - f.e. -> path.to.my.package.TestClassST
* @param classes Map that should be updated with test-classes info
* @param packagePath path on which the files and classes should be listed
* @param generatePath "prefix" path where the documentation should be generated into
* @return updated Map with test-classes info from the {@param packagePath}
*/
private static Map<String, String> getClassesForPackage(Map<String, String> classes, Path packagePath, String generatePath) {
try {
Files.list(packagePath)
.forEach(path -> {
if (Files.isDirectory(path)) {
classes.putAll(getClassesForPackage(classes, path, generatePath));
} else {
String classPackagePath = path.toAbsolutePath().toString().replaceAll(REMOVE_BEFORE_PACKAGE.toString(), "").replace(".java", "");
classes.put(generatePath + classPackagePath, classPackagePath.replaceAll("/", "."));
}
});
} catch (IOException exception) {
throw new RuntimeException(exception);
}

return classes;
}
}
Loading

0 comments on commit 0957563

Please sign in to comment.