Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework unit tests #124

Merged
merged 13 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
* text=auto

*.bat text eol=crlf

*.approved.* binary
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ build
.project
.classpath

src/test/**/*.received.txt

tests/**/*/analysis.json
tests/**/*/tags.json
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ Then, run the Java analyzer using `build/libs/java-analyzer.jar`.
For example, to analyze a solution for the `leap` exercise, run:

```sh
java -jar build/libs/java-analyzer.jar leap /path/to/leap/ /path/to/output/folder/
java -jar build/libs/java-analyzer.jar leap /path/to/leap /path/to/output/folder
```

The analyzer output is written to `analysis.json` and `tags.json` in `/path/to/output/folder/`.
The analyzer output is written to `analysis.json` and `tags.json` in `/path/to/output/folder`.

### Running with Docker

Expand All @@ -39,10 +39,10 @@ Then, run the image and mount the directory of the solution to analyze.
For example, to analyze a solution for the `leap` exercise located at `~/exercism/java/leap`, run:

```sh
docker run -v /path/to/leap:/input -v /path/to/output/folder:/output exercism/java-analyzer leap /input/ /output/
docker run -v /path/to/leap:/input -v /path/to/output/folder:/output exercism/java-analyzer leap /input /output
```

The analyzer output is written to `analysis.json` and `tags.json` in `/path/to/output/folder/`.
The analyzer output is written to `analysis.json` and `tags.json` in `/path/to/output/folder`.

## Tests

Expand Down
4 changes: 2 additions & 2 deletions bin/run-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
# The test results are formatted according to the specifications at https://github.com/exercism/docs/blob/main/building/tooling/analyzers/interface.md

# Example:
# ./bin/run-in-docker.sh two-fer /absolute/path/to/two-fer/solution/folder/ /absolute/path/to/output/directory/
# ./bin/run-in-docker.sh two-fer /absolute/path/to/two-fer/solution/folder /absolute/path/to/output/directory

# If any required arguments is missing, print the usage and exit
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
echo "usage: ./bin/run-in-docker.sh exercise-slug /absolute/path/to/solution/folder/ /absolute/path/to/output/directory/"
echo "usage: ./bin/run-in-docker.sh exercise-slug /absolute/path/to/solution/folder /absolute/path/to/output/directory"
exit 1
fi

Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ repositories {
}

dependencies {
implementation "org.json:json:20231013"
implementation "com.google.code.gson:gson:2.10.1"
implementation "com.github.javaparser:javaparser-core:3.25.8"

testImplementation platform("org.junit:junit-bom:5.10.1")
testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "org.assertj:assertj-core:3.25.2"
testImplementation "com.approvaltests:approvaltests:22.3.2"
}

shadowJar {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/analyzer/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
*/
public interface Analyzer {
/**
* Analyze the given solution and append analysis results to the given analysis.
* Analyze the given solution and append analysis results to the given output..
* The {@code analyze} method of each analyzer is invoked once for the whole submitted solution.
*
* @param solution The solution that should be analyzed.
* @param analysis The analysis instance used to collect results.
* @param output The output collector instance used to collect analyzer results.
* This instance is shared across all analyzers, and should be used to add comments and tags,
* or set a summary.
*/
void analyze(Solution solution, Analysis analysis);
void analyze(Solution solution, OutputCollector output);
}
40 changes: 16 additions & 24 deletions src/main/java/analyzer/AnalyzerCli.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package analyzer;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;

Expand All @@ -10,13 +9,19 @@
* The CLI expects three arguments and is used like this:
*
* <pre>
* java -jar java-analyzer.jar exercise-slug /path/to/input/ /path/to/output/
* java -jar java-analyzer.jar exercise-slug /path/to/input /path/to/output
* </pre>
*/
public class AnalyzerCli {

private static boolean isNotValidDirectory(String p) {
return !p.endsWith("/") || !new File(p).isDirectory();
private static Path validateDirectory(String directory) {
var file = new File(directory);

if (!file.exists() || !file.isDirectory()) {
throw new IllegalArgumentException("Not a valid directory: " + directory);
}

return file.toPath();
}

public static void main(String... args) throws IOException {
Expand All @@ -25,26 +30,13 @@ public static void main(String... args) throws IOException {
System.exit(-1);
}

String slug = args[0];
String inputDirectory = args[1];
String outputDirectory = args[2];

if (isNotValidDirectory(inputDirectory)) {
System.err.println("Invalid input directory. Must be a valid directory and end with a slash.");
System.exit(-1);
}
if (isNotValidDirectory(outputDirectory)) {
System.err.println("Invalid output directory. Must be a valid directory and end with a slash.");
System.exit(-1);
}

var solution = new SubmittedSolution(slug, Path.of(inputDirectory));
var analysis = AnalyzerRoot.analyze(solution);
var slug = args[0];
var inputDirectory = validateDirectory(args[1]);
var outputDirectory = validateDirectory(args[2]);

try (var analysisWriter = new FileWriter(Path.of(outputDirectory, "analysis.json").toFile());
var tagsWriter = new FileWriter(Path.of(outputDirectory, "tags.json").toFile())) {
var output = new OutputWriter(analysisWriter, tagsWriter);
output.write(analysis);
}
var outputWriter = new OutputWriter(outputDirectory);
var solution = new SubmittedSolution(slug, inputDirectory);
var output = AnalyzerRoot.analyze(solution);
outputWriter.write(output);
}
}
14 changes: 7 additions & 7 deletions src/main/java/analyzer/AnalyzerRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ private AnalyzerRoot() {
* Perform the analysis of a solution.
*
* @param solution The solution being analyzed.
* @return The aggregated analysis of all applicable analyzers.
* @return The aggregated output of all applicable analyzers.
*/
public static Analysis analyze(Solution solution) {
var analysis = new Analysis();
public static Output analyze(Solution solution) {
var outputBuilder = new OutputBuilder();

for (Analyzer analyzer : createAnalyzers(solution.getSlug())) {
analyzer.analyze(solution, analysis);
analyzer.analyze(solution, outputBuilder);
}

if (analysis.getComments().stream().anyMatch(x -> x.getType() != Comment.Type.CELEBRATORY)) {
analysis.addComment(new FeedbackRequest());
if (outputBuilder.getComments().stream().anyMatch(x -> x.getType() != Comment.Type.CELEBRATORY)) {
outputBuilder.addComment(new FeedbackRequest());
}

return analysis;
return outputBuilder.build();
}

private static List<Analyzer> createAnalyzers(String slug) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/analyzer/Output.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package analyzer;

import java.util.List;

public record Output(Analysis analysis, Tags tags) {

public record Analysis(String summary, List<Comment> comments) {
}

public record Tags(List<String> tags) {
}
}
49 changes: 49 additions & 0 deletions src/main/java/analyzer/OutputBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package analyzer;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

class OutputBuilder implements OutputCollector {
private String summary;
private final Set<Comment> comments = new LinkedHashSet<>();
private final Set<String> tags = new LinkedHashSet<>();

public String getSummary() {
return summary;
}

public void setSummary(String summary) {
this.summary = summary;
}

public List<Comment> getComments() {
return List.copyOf(comments);
}

public List<String> getTags() {
return List.copyOf(tags);
}

public void addComment(Comment comment) {
comments.add(comment);
}

public void addTag(String tag) {
tags.add(tag);
}

Output build() {
var sortedComments = this.comments.stream().sorted(OutputBuilder::compareCommentsByType).toList();
var analysis = new Output.Analysis(this.summary, sortedComments);
var tags = new Output.Tags(List.copyOf(this.tags));
return new Output(analysis, tags);
}

private static int compareCommentsByType(Comment a, Comment b) {
var ordinalA = Optional.ofNullable(a.getType()).map(Comment.Type::ordinal).orElse(Integer.MAX_VALUE);
var ordinalB = Optional.ofNullable(b.getType()).map(Comment.Type::ordinal).orElse(Integer.MAX_VALUE);
return Integer.compare(ordinalA, ordinalB);
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
package analyzer;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* This class is used to collect analysis results in the form of comments, tags and an optional summary.
* This interface is used to collect analyzer output in the form of comments, tags and an optional summary.
*
* @see <a href="https://exercism.org/docs/building/tooling/analyzers/interface">The analyzer interface in the Exercism documentation</a>
*/
public class Analysis {
private String summary;
private final Set<Comment> comments = new LinkedHashSet<>();
private final Set<String> tags = new LinkedHashSet<>();
public interface OutputCollector {

/**
* The summary is a short description of the complete analysis result.
* It is {@code null} by default.
*
* @return The summary if set, {@code null} otherwise.
*/
public String getSummary() {
return summary;
}
String getSummary();

/**
* Set the summary of the analysis.
Expand All @@ -31,47 +24,37 @@ public String getSummary() {
*
* @param summary The summary to set.
*/
public void setSummary(String summary) {
this.summary = summary;
}
void setSummary(String summary);

/**
* Retrieve a copy of the comments added to this analysis.
* The resulting list is guaranteed to contain no duplicates.
*
* @return List of comments.
*/
public List<Comment> getComments() {
return List.copyOf(comments);
}
List<Comment> getComments();

/**
* Retrieve a copy of the tags added to this analysis.
* The resulting list is guaranteed to contain no duplicates.
*
* @return List of tags.
*/
public List<String> getTags() {
return List.copyOf(tags);
}
List<String> getTags();

/**
* Add a new comment to the analysis.
* This does nothing if a comment with the same values was added previously.
*
* @param comment The comment to add.
*/
public void addComment(Comment comment) {
comments.add(comment);
}
void addComment(Comment comment);

/**
* Add a new tag to the analysis.
* This does nothing if the same tag was added previously.
*
* @param tag The tag to add.
*/
public void addTag(String tag) {
tags.add(tag);
}
void addTag(String tag);
}
48 changes: 48 additions & 0 deletions src/main/java/analyzer/OutputSerializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package analyzer;

import com.google.gson.*;

import java.lang.reflect.Type;
import java.util.Map;
import java.util.TreeMap;

/**
* Serializer to convert the analyzer output to JSON.
*
* @see <a href="https://exercism.org/docs/building/tooling/analyzers/interface">The analyzer interface in the Exercism documentation</a>
*/
class OutputSerializer {
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(Comment.class, new CommentJsonSerializer())
.setPrettyPrinting()
.create();

static String serialize(Output.Analysis analysis) {
return GSON.toJson(analysis);
}

static String serialize(Output.Tags tags) {
return GSON.toJson(tags);
}

private static class CommentJsonSerializer implements JsonSerializer<Comment> {
@Override
public JsonElement serialize(Comment comment, Type type, JsonSerializationContext jsonSerializationContext) {
var json = new JsonObject();
json.addProperty("comment", comment.getKey());
json.add("params", serializeParameters(comment.getParameters()));

if (comment.getType() != null) {
json.addProperty("type", comment.getType().name().toLowerCase());
}

return json;
}

private static JsonElement serializeParameters(Map<String, String> parameters) {
var json = new JsonObject();
new TreeMap<>(parameters).forEach(json::addProperty);
return json;
}
}
}
Loading