Skip to content

Commit

Permalink
Adding TestResultsLogger
Browse files Browse the repository at this point in the history
This class watches JUnit test progress and outputs the results to a file
  • Loading branch information
emichael committed Jul 11, 2023
1 parent 4105430 commit c601de0
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 7 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ dependencies {
graderImplementation group: 'commons-io', name: 'commons-io', version: '2.13.0'
graderSources group: 'commons-io', name: 'commons-io', version: '2.13.0', classifier: 'sources'

graderImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2'
graderSources group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2', classifier: 'sources'
graderImplementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.15.2'
graderImplementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: '2.15.2'

// Swing stuff
graderImplementation group: 'com.formdev', name: 'flatlaf', version: '2.0.1'
graderImplementation group: 'com.formdev', name: 'flatlaf', version: '2.0.1', classifier: 'sources'
Expand Down
31 changes: 30 additions & 1 deletion framework/tst/dslabs/framework/testing/junit/TestResults.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@

package dslabs.framework.testing.junit;

import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.time.Instant;
import javax.annotation.Nullable;
Expand All @@ -32,15 +42,18 @@
import lombok.NonNull;
import lombok.Singular;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;

@Value
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Jacksonized
@JsonIgnoreProperties
class TestResults implements Serializable {

@Value
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Jacksonized
static class TestResult implements Serializable {
String labName;
Integer part;
Expand Down Expand Up @@ -72,7 +85,9 @@ public boolean failed() {
}

@Value
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Jacksonized
static class TestFailure implements Serializable {
@NonNull Instant time;
@NonNull FailureReason reason;
Expand All @@ -85,4 +100,18 @@ static class TestFailure implements Serializable {

Instant startTime;
Instant endTime;

private static void configureObjectMapper(ObjectMapper mapper) {
// Pretty-print XML file
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new JavaTimeModule());
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
}

public void writeJsonToFile(String fileName) throws IOException {
JsonMapper mapper = new JsonMapper();
configureObjectMapper(mapper);
mapper.writeValue(new File(fileName), this);
}
}
152 changes: 152 additions & 0 deletions framework/tst/dslabs/framework/testing/junit/TestResultsLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright (c) 2023 Ellis Michael ([email protected])
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package dslabs.framework.testing.junit;


import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import dslabs.framework.testing.junit.TestResults.TestFailure;
import dslabs.framework.testing.junit.TestResults.TestResult;
import dslabs.framework.testing.junit.TestResults.TestResult.TestResultBuilder;
import dslabs.framework.testing.junit.TestResults.TestResultsBuilder;
import dslabs.framework.testing.utils.GlobalSettings;
import dslabs.framework.testing.utils.TeeStdOutErr;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import org.junit.experimental.categories.Category;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;

/**
* Records results of test runs and, if enabled, logs them to a file.
*/
class TestResultsLogger extends RunListener {
private static boolean isListening = false;

private final TestResultsBuilder testResultsBuilder = TestResults.builder();
private TestResultBuilder testResultBuilder = null;

private static int testNumber(Description d) {
assert d.isTest();
String n = d.getMethodName();
return Integer.parseInt(n.replaceFirst("test(\\d+)\\w+", "$1"));
}

@Override
public void testRunStarted(Description description) throws Exception {
// Can only run one instance of the listener at a time
assert !isListening;
isListening = true;
testResultsBuilder.startTime(Instant.now());
}

@Override
public void testRunFinished(Result result) throws IOException {
testResultsBuilder.endTime(Instant.now());
TestResults results = testResultsBuilder.build();

// Write out the results to a file if enabled
if (GlobalSettings.testResultsOutputFile() != null) {
results.writeJsonToFile(GlobalSettings.testResultsOutputFile());
}

assert isListening;
isListening = false;
}

@Override
public void testStarted(Description description)
throws NoSuchMethodException {
testResultBuilder = TestResult.builder();
testResultBuilder.startTime(Instant.now());

Class<?> testClass = description.getTestClass();

Lab labName = testClass.getAnnotation(Lab.class);
if (labName != null) {
testResultBuilder.labName(labName.value());
}

Part part = testClass.getAnnotation(Part.class);
if (part != null) {
testResultBuilder.part(part.value());
}

testResultBuilder.testNumber(testNumber(description));

TestDescription testDescription =
description.getAnnotation(TestDescription.class);
if (testDescription != null) {
testResultBuilder.testDescription(testDescription.value());
}

TestPointValue testPointValue =
description.getAnnotation(TestPointValue.class);
if (testPointValue != null) {
testResultBuilder.pointValue(testPointValue.value());
}

testResultBuilder.testMethodName(description.getMethodName());

Category categories = description.getAnnotation(Category.class);
if (categories != null) {
testResultBuilder.testCategories(
Arrays.stream(categories.value()).map(Class::getName)
.collect(ImmutableList.toImmutableList()));
}

// Install tees for System.out and System.err
TeeStdOutErr.installTees();
}

@Override
public void testFailure(Failure failure) {
Throwable t = failure.getException();
// TODO: parse this correctly here and add a reason and the right time
TestFailure testFailure =
TestFailure.builder().reason(FailureReason.UNKNOWN)
.time(Instant.now())
.exceptionClass(t.getClass().getName())
.message(t.getMessage())
.stackTrace(Throwables.getStackTraceAsString(t))
.build();
testResultBuilder.testFailure(testFailure);
}

@Override
public void testFinished(Description description) {
// Log the test result
testResultBuilder.endTime(Instant.now());

// TODO: enable/disable this and the installTees call with a flag
var tee = TeeStdOutErr.clearTees();
testResultBuilder.stdOutLog(tee.stdOut());
testResultBuilder.stdErrLog(tee.stdErr());

testResultsBuilder.result(testResultBuilder.build());
testResultBuilder = null;
}
}
13 changes: 9 additions & 4 deletions framework/tst/dslabs/framework/testing/utils/GlobalSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.Setter;

Expand All @@ -39,12 +40,12 @@ public final class GlobalSettings {
private static final String TEST_NUM = "testNum", LOG_LEVEL = "logLevel";

@Getter private static final boolean verbose =
Boolean.parseBoolean(lookupWithDefault("verbose", "true")),
Boolean.parseBoolean(lookupWithDefault("verbose", "true"));

singleThreaded =
Boolean.parseBoolean(lookupWithDefault("singleThreaded", "false")),
@Getter private static final boolean singleThreaded =
Boolean.parseBoolean(lookupWithDefault("singleThreaded", "false"));

startVisualization =
@Getter private static final boolean startVisualization =
Boolean.parseBoolean(lookupWithDefault("startViz", "false"));

@Getter @Setter private static boolean saveTraces =
Expand All @@ -53,6 +54,10 @@ public final class GlobalSettings {
private static final boolean doChecks =
Boolean.parseBoolean(lookupWithDefault("doChecks", "false"));

/* The file to print the JSON-ized test results to. */
@Getter @Nullable private static final String testResultsOutputFile =
lookupWithDefault("resultsOutputFile", null);

@Setter private static boolean errorChecksTemporarilyEnabled = false;

private static final boolean timeoutsDisabled = Boolean.parseBoolean(
Expand Down
10 changes: 8 additions & 2 deletions handout-files/run-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def make():
def run_tests(lab, part=None, no_run=False, no_search=False,
timers_disabled=False, log_level=None, single_threaded=False,
start_viz=False, do_checks=False, test_num=None, assertions=False,
save_traces=False):
save_traces=False, output_file=None):
"""Run the specified tests."""
make()

Expand All @@ -84,6 +84,9 @@ def run_tests(lab, part=None, no_run=False, no_search=False,
if save_traces:
command.append('-DsaveTraces=true')

if output_file:
command.append(f'-DresultsOutputFile={output_file}')

command += [
'-cp',
RUNTIME_CLASSPATH,
Expand Down Expand Up @@ -226,6 +229,8 @@ def main():
help="start the visualization on invariant violation "
"or when the search is unable to find a "
"particular state")
parser.add_argument('-o', '--output-file', default=None,
help="file to print JSON-serialized test results to")

group.add_argument('-d', '--debugger', nargs='*', metavar="ARG",
help="Don't run any tests, instead start the visual "
Expand Down Expand Up @@ -308,7 +313,8 @@ def disallow_arguments(current_option, disallowed_options):
do_checks=args.checks,
test_num=args.test_num,
assertions=args.assertions,
save_traces=args.save_traces)
save_traces=args.save_traces,
output_file=args.output_file)


if __name__ == '__main__':
Expand Down

0 comments on commit c601de0

Please sign in to comment.