Skip to content

Commit

Permalink
Introducing a test framework for Java APIView processor (#8115)
Browse files Browse the repository at this point in the history
Introducing a test framework for Java APIView processor to compare output for published libraries against their known-good representations, to help avoid regrettable regressions. There are no test outputs in place yet - they will be added after the token refactoring is done.
  • Loading branch information
JonathanGiles authored Apr 17, 2024
1 parent 93fbd66 commit 5bac009
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
Expand All @@ -33,7 +34,7 @@
public class Main {
// expected argument order:
// [inputFiles] <outputDirectory>
public static void main(String[] args) throws IOException {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Expected argument order: [comma-separated sources jarFiles] <outputFile>, e.g. /path/to/jarfile.jar ./temp/");
System.exit(-1);
Expand All @@ -43,28 +44,35 @@ public static void main(String[] args) throws IOException {
final String[] jarFilesArray = jarFiles.split(",");

final File outputDir = new File(args[1]);
if (!outputDir.exists()) {
if (!outputDir.mkdirs()) {
System.out.printf("Failed to create output directory %s%n", outputDir);
}
}

System.out.println("Running with following configuration:");
System.out.printf(" Output directory: '%s'%n", outputDir);

for (final String jarFile : jarFilesArray) {
System.out.printf(" Processing input .jar file: '%s'%n", jarFile);
Arrays.stream(jarFilesArray).forEach(jarFile -> run(new File(jarFile), outputDir));
}

final File file = new File(jarFile);
if (!file.exists()) {
System.out.printf("Cannot find file '%s'%n", file);
/**
* Runs APIView parser and returns the output file path.
*/
public static File run(File jarFile, File outputDir) {
System.out.printf(" Processing input .jar file: '%s'%n", jarFile);

if (!jarFile.exists()) {
System.out.printf("Cannot find file '%s'%n", jarFile);
System.exit(-1);
}

if (!outputDir.exists()) {
if (!outputDir.mkdirs()) {
System.out.printf("Failed to create output directory %s%n", outputDir);
System.exit(-1);
}

final String jsonFileName = file.getName().substring(0, file.getName().length() - 4) + ".json";
final File outputFile = new File(outputDir, jsonFileName);
processFile(file, outputFile);
}

final String jsonFileName = jarFile.getName().substring(0, jarFile.getName().length() - 4) + ".json";
final File outputFile = new File(outputDir, jsonFileName);
processFile(jarFile, outputFile);
return outputFile;
}

private static ReviewProperties getReviewProperties(File inputFile) {
Expand Down Expand Up @@ -104,7 +112,7 @@ private static ReviewProperties getReviewProperties(File inputFile) {
return reviewProperties;
}

private static void processFile(final File inputFile, final File outputFile) throws IOException {
private static void processFile(final File inputFile, final File outputFile) {
final APIListing apiListing = new APIListing();

// empty tokens list that we will fill as we process each class file
Expand All @@ -126,13 +134,22 @@ private static void processFile(final File inputFile, final File outputFile) thr
"that was submitted to APIView was named " + inputFile.getName()));
}

// Write out to the filesystem
try (JsonWriter jsonWriter = JsonProviders.createWriter(Files.newBufferedWriter(outputFile.toPath()))) {
apiListing.toJson(jsonWriter);
try {
// Write out to the filesystem, make the file if it doesn't exist
if (!outputFile.exists()) {
if (!outputFile.createNewFile()) {
System.out.printf("Failed to create output file %s%n", outputFile);
}
}
try (JsonWriter jsonWriter = JsonProviders.createWriter(Files.newBufferedWriter(outputFile.toPath()))) {
apiListing.toJson(jsonWriter);
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static void processJavaSourcesJar(File inputFile, APIListing apiListing) throws IOException {
private static void processJavaSourcesJar(File inputFile, APIListing apiListing) {
final ReviewProperties reviewProperties = getReviewProperties(inputFile);

final String groupId = reviewProperties.getMavenPom().getGroupId();
Expand Down Expand Up @@ -260,8 +277,6 @@ private static void processXmlFile(File inputFile, APIListing apiListing) {
} catch (IOException e) {
e.printStackTrace();
}


}

private static class ReviewProperties {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.azure.tools.apiview;

import com.azure.tools.apiview.processor.Main;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;

import java.net.URL;
import java.nio.file.*;
import java.io.*;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

public class TestExpectedOutputs {
// This is a flag to add a '-DELETE' suffix to the generated output file if the expected output file is missing.
// By default we do, but if we want to regenerate a bunch of expected outputs, we can see this to false to save
// having to rename.
private static final boolean ADD_DELETE_PREFIX_FOR_MISSING_EXPECTED_OUTPUTS = true;

private static final String root = "src/test/resources/";
private static final File tempDir = new File(root + "temp");

private static Stream<String> provideFileNames() {
// This is a stream of local files or URLs to download the file from (or a combination of both). For each value,
// if it starts with http, it'll try downloading the file into a temp location. If it sees no http, it'll look
// for the file in the inputs directory. To prevent clogging up the repository with source jars, it is
// preferable to download the source jars from known-good places.
return Stream.of(
""
// "https://repo1.maven.org/maven2/com/azure/azure-core/1.48.0/azure-core-1.48.0-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-communication-chat/1.5.0/azure-communication-chat-1.5.0-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-security-keyvault-keys/4.8.2/azure-security-keyvault-keys-4.8.2-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-data-appconfiguration/1.6.0/azure-data-appconfiguration-1.6.0-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-messaging-eventhubs/5.18.2/azure-messaging-eventhubs-5.18.2-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-messaging-servicebus/7.15.2/azure-messaging-servicebus-7.15.2-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-identity/1.12.0/azure-identity-1.12.0-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-storage-blob/12.25.3/azure-storage-blob-12.25.3-sources.jar",
// "https://repo1.maven.org/maven2/com/azure/azure-cosmos/4.57.0/azure-cosmos-4.57.0-sources.jar"
);
}

@BeforeAll
public static void setup() {
tempDir.mkdirs();
}

@ParameterizedTest
@MethodSource("provideFileNames")
public void testGeneratedJson(String filenameOrUrl) {
if (filenameOrUrl.isEmpty()) {
return;
}

String filename;
Path inputFile;

if (filenameOrUrl.startsWith("http")) {
// download the file if it isn't local...
filename = filenameOrUrl.substring(filenameOrUrl.lastIndexOf('/') + 1);

// download the file and save it to the temp directory
String destinationFile = tempDir + "/" + filename;
downloadFile(filenameOrUrl, destinationFile);

// strip off the file extension
filename = filename.substring(0, filename.lastIndexOf('.'));
inputFile = Paths.get(destinationFile);
} else {
// the file is local, so we can go straight to it
filename = filenameOrUrl;
inputFile = Paths.get(root + "inputs/" + filename + ".jar");
}

final Path expectedOutputFile = Paths.get(root + "expected-outputs/" + filename + ".json");

// Run the processor, receiving the name of the file that was generated
final Path actualOutputFile = Main.run(inputFile.toFile(), tempDir).toPath();

if (expectedOutputFile.toFile().exists()) {
try {
// Compare the temporary file to the expected-outputs file
assertArrayEquals(Files.readAllBytes(expectedOutputFile), Files.readAllBytes(actualOutputFile));
} catch (IOException e) {
fail("Failed to compare the actual output file with the expected output file.", e);
}
} else {
// the test was never going to pass as we don't have an expected file to compare against.
try {
final String outputFile = root + "expected-outputs/" + filename + (ADD_DELETE_PREFIX_FOR_MISSING_EXPECTED_OUTPUTS ? "-DELETE" : "") + ".json";
final Path suggestedOutputFile = Paths.get(outputFile);

if (!suggestedOutputFile.toFile().getParentFile().exists()) {
suggestedOutputFile.toFile().getParentFile().mkdirs();
}
Files.move(actualOutputFile, suggestedOutputFile, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
// unable to move file
e.printStackTrace();
}
if (ADD_DELETE_PREFIX_FOR_MISSING_EXPECTED_OUTPUTS) {
fail("Could not find expected output file for test. The output from the sources.jar was added into the " +
"/expected-outputs directory, but with a '-DELETE' filename suffix. Please review and rename if it " +
"should be included in the test suite.");
} else {
fail("Could not find expected output file for test. The output from the sources.jar was added into the " +
"/expected-outputs directory without any -DELETE suffix. It will be used next time the test is run!");

}
}
}

@AfterAll
public static void tearDown() throws IOException {
// Delete the temporary file
if (tempDir.exists()) {
// delete everything in tempDir and then delete the directory
Files.walk(tempDir.toPath())
.map(Path::toFile)
.forEach(File::delete);

try {
Files.delete(tempDir.toPath());
} catch (NoSuchFileException e) {
// ignore
}
}
}

private static void downloadFile(String urlStr, String destinationFile) {
File file = new File(destinationFile);
if (file.exists()) {
return;
}
try (InputStream in = new URL(urlStr).openStream()) {
Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
fail("Failed to download and / or copy the given URL to the given destination { url: "
+ urlStr + ", destination: " + destinationFile + " }");
}
}
}

0 comments on commit 5bac009

Please sign in to comment.