Skip to content

Commit

Permalink
Rework unit tests (#124)
Browse files Browse the repository at this point in the history
* Reimplement unit tests to use Approvals library

* Add OutputCollectorTest

* Convert OutputCollector to interface

* Merge analyzer tests into AnalyzerIntegrationTest

* Add Comments unit tests

* Update Javadoc

* Add unit test for SubmittedSolution class

* Add Javadoc to test helper classes

* Remove checks for CLI argument paths ending with /

* Make sure there are at least two smoke tests per exercise

* Update AnalyzerIntegrationTest after merge

* Fix compilation error after resolving conflicts
  • Loading branch information
sanderploegsma authored Feb 14, 2024
1 parent d7b8abe commit e389344
Show file tree
Hide file tree
Showing 148 changed files with 1,556 additions and 650 deletions.
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

0 comments on commit e389344

Please sign in to comment.