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

Branch for working on rust support #12 #28

Merged
merged 13 commits into from
Sep 13, 2015
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ addons:
- check
- python3.4
before_install:
- curl -L https://static.rust-lang.org/rustup.sh | sh -s -- --channel=stable --yes --prefix=$PWD --disable-sudo
- export PATH=$PATH:$PWD/bin
- export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib
- mkdir $HOME/bin && ln -s $(which python3.4) $HOME/bin/python3 && export PATH="$HOME/bin:$PATH"
script:
- mvn checkstyle:check
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package fi.helsinki.cs.tmc.langs.rust;

import fi.helsinki.cs.tmc.langs.AbstractLanguagePlugin;
import fi.helsinki.cs.tmc.langs.abstraction.ValidationResult;
import fi.helsinki.cs.tmc.langs.domain.Configuration;
import fi.helsinki.cs.tmc.langs.domain.ExerciseBuilder;
import fi.helsinki.cs.tmc.langs.domain.ExerciseDesc;
import fi.helsinki.cs.tmc.langs.domain.RunResult;
import fi.helsinki.cs.tmc.langs.domain.RunResult.Status;
import fi.helsinki.cs.tmc.langs.domain.SpecialLogs;
import fi.helsinki.cs.tmc.langs.domain.TestResult;
import fi.helsinki.cs.tmc.langs.io.StudentFilePolicy;
import fi.helsinki.cs.tmc.langs.io.sandbox.StudentFileAwareSubmissionProcessor;
import fi.helsinki.cs.tmc.langs.io.zip.StudentFileAwareUnzipper;
import fi.helsinki.cs.tmc.langs.io.zip.StudentFileAwareZipper;
import fi.helsinki.cs.tmc.langs.rust.util.Constants;
import fi.helsinki.cs.tmc.langs.utils.ProcessResult;
import fi.helsinki.cs.tmc.langs.utils.ProcessRunner;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;

public class CargoPlugin extends AbstractLanguagePlugin {

private static final Logger log = LoggerFactory.getLogger(CargoPlugin.class);
private static final RunResult EMPTY_FAILURE = new RunResult(
Status.COMPILE_FAILED,
ImmutableList.<TestResult>of(),
new ImmutableMap.Builder<String, byte[]>().build());

public CargoPlugin() {
super(
new ExerciseBuilder(),
new StudentFileAwareSubmissionProcessor(),
new StudentFileAwareZipper(),
new StudentFileAwareUnzipper());
}

@Override
public boolean isExerciseTypeCorrect(Path path) {
return Files.isRegularFile(path.resolve(Constants.CARGO_TOML));
}

@Override
protected StudentFilePolicy getStudentFilePolicy(Path projectPath) {
return new CargoStudentFilePolicy(projectPath);
}

@Override
public ValidationResult checkCodeStyle(Path path) {
throw new UnsupportedOperationException("Not supported yet.");
}

@Override
public String getLanguageName() {
return "cargo";
}

public String getPluginName() {
return "cargo";
}

@Override
public Optional<ExerciseDesc> scanExercise(Path path, String exerciseName) {
throw new UnsupportedOperationException("Not supported yet.");
}

@Override
public RunResult runTests(Path path) {
Optional<RunResult> result = build(path);
if (result.isPresent()) {
log.info("Failed to compile project.");
return result.get();
}
return runBuiltTests(path);
}

private Optional<RunResult> build(Path path) {
String[] command = {"cargo", "test", "--no-run"};
log.info("Building project with command {0}", Arrays.deepToString(command));
Optional<ProcessResult> result = run(command, path);
if (result.isPresent()) {
if (result.get().statusCode == 0) {
return Optional.absent();
}
return Optional.of(filledFailure(result.get()));
}
return Optional.of(EMPTY_FAILURE);
}

private RunResult runBuiltTests(Path dir) {
String[] command = {"cargo", "test"};
log.info("Running tests with command {0}", Arrays.deepToString(command));
Optional<ProcessResult> result = run(command, dir);
if (result.isPresent()) {
return parseResult(result.get(), dir);
}
return EMPTY_FAILURE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how it is currently done elsewhere but returning empty value instead of error/exception omits the error (or is this just backup solution...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How it would look to the end user if the cargo test fails

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo test failing here would mean that there was an IOException while running the command. I am not sure what situation this would happen that wouldn't already happen while building tests. And compilation output when compilation failure happens is already preserved.
Dunno how to how to deal this.

}

private Optional<ProcessResult> run(String[] command, Path dir) {
ProcessRunner runner = new ProcessRunner(command, dir);
try {
return Optional.of(runner.call());
} catch (Exception e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just catch relevant exceptions and at least log those

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the call function actually just throws an Exception... I'll add logging there though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps also log which command failed, since we could theoretically be running multiple concurrent runs?

log.error("Running command {0} failed {1}", Arrays.deepToString(command), e);
return Optional.absent();
}
}

private RunResult filledFailure(ProcessResult processResult) {
byte[] errorOutput = processResult.errorOutput.getBytes(StandardCharsets.UTF_8);
ImmutableMap<String, byte[]> logs = new ImmutableMap.Builder()
.put(SpecialLogs.COMPILER_OUTPUT, errorOutput)
.<String, byte[]>build();
return new RunResult(
Status.COMPILE_FAILED,
ImmutableList.<TestResult>of(),
logs);
}

private RunResult parseResult(ProcessResult processResult, Path path) {
return new CargoResultParser().parse(processResult);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package fi.helsinki.cs.tmc.langs.rust;

import fi.helsinki.cs.tmc.langs.domain.RunResult;
import fi.helsinki.cs.tmc.langs.domain.RunResult.Status;
import fi.helsinki.cs.tmc.langs.domain.SpecialLogs;
import fi.helsinki.cs.tmc.langs.domain.TestResult;
import fi.helsinki.cs.tmc.langs.utils.ProcessResult;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Parses Cargos test output.
*/
public class CargoResultParser {

private static final RunResult PARSING_FAILED = new RunResult(
Status.GENERIC_ERROR,
ImmutableList.<TestResult>of(),
new ImmutableMap.Builder<String, byte[]>()
.put("generic_error_message", "Test results couldn't be parsed."
.getBytes(StandardCharsets.UTF_8))
.build());

//test result: FAILED. 25 passed; 1 failed; 0 ignored; 0 measured
private static final Pattern RESULT
= Pattern.compile("test result: .*\\. (?<passes>\\d*) passed; (?<fails>\\d*) failed; \\d* ignored; \\d* measured");
//test test::dim4::test_4d_1_80_perioditic ... ok
private static final Pattern TEST = Pattern.compile("test (?<name>.*) \\.\\.\\. .*");
//thread 'test::dim2::test_2d_1_80_normal' panicked at 'assertion failed: false', src\test\dim2.rs:12
private static final Pattern FAILURES
= Pattern.compile(".*thread '(?<name>.*)' panicked at '(?<description>.*)', .*");

/**
* Parses given process results outputs to a run result.
*/
public RunResult parse(ProcessResult processResult) {
String output = processResult.output;
String[] lines = output.split("\\r?\\n");

Matcher matcher = RESULT.matcher(output);
while (matcher.find()) {
int fails = Integer.parseInt(matcher.group("fails"));
int passes = Integer.parseInt(matcher.group("passes"));
if (fails + passes > 0) {
Optional<Map<String, String>> failures
= findFailures(processResult.output, fails);
if (failures.isPresent()) {
Status status = fails == 0 ? Status.PASSED : Status.TESTS_FAILED;
List<String> tests = parseResults(lines);
return new RunResult(
status,
buildTestResults(tests, failures.get()),
new ImmutableMap.Builder()
.put(SpecialLogs.STDOUT,
processResult.output.getBytes(StandardCharsets.UTF_8))
.put(SpecialLogs.STDERR,
processResult.errorOutput.getBytes(StandardCharsets.UTF_8))
.build());
}
break;
}
}
return PARSING_FAILED;
}

private List<String> parseResults(String[] lines) {
List<String> result = new ArrayList<>();
for (String line : lines) {
Matcher matcher = TEST.matcher(line);
if (matcher.matches()) {
result.add(matcher.group("name"));
}
}
return result;
}

private Optional<Map<String, String>> findFailures(String errorOutput, int fails) {
Map<String, String> result = new HashMap<>();
String[] lines = errorOutput.split("\\r?\\n");
for (String line : lines) {
Matcher matcher = FAILURES.matcher(line);
if (matcher.matches()) {
result.put(matcher.group("name"), matcher.group("description"));
}
}
return Optional.of(result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will never return Optional.absent(), since result is always non-null. Unwrap from Optional?

}

private ImmutableList<TestResult> buildTestResults(List<String> results,
Map<String, String> failures) {
ImmutableList.Builder<TestResult> testResults = ImmutableList.builder();
for (String test : results) {
String description = failures.get(test);
boolean pass = false;
if (description == null) {
description = "";
pass = true;
}
TestResult testResult = new TestResult(
test,
pass,
ImmutableList.<String>of(),
description,
ImmutableList.<String>of());
testResults.add(testResult);
}
return testResults.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package fi.helsinki.cs.tmc.langs.rust;

import fi.helsinki.cs.tmc.langs.io.ConfigurableStudentFilePolicy;
import fi.helsinki.cs.tmc.langs.rust.util.Constants;

import java.nio.file.Path;

public class CargoStudentFilePolicy extends ConfigurableStudentFilePolicy {

public CargoStudentFilePolicy(Path configFileParent) {
super(configFileParent);
}

/**
* Returns {@code true} for all files in the <tt>projectRoot/src</tt> directory and other
* files required for building the project.
*
* <p>Will NOT return {@code true} for any test files. If test file modification are part
* of the exercise, those test files are whitelisted as <tt>ExtraStudentFiles</tt> and the
* decision to move them is made by {@link ConfigurableStudentFilePolicy}.
*/
@Override
public boolean isStudentSourceFile(Path path) {
return !path.endsWith(Constants.CARGO_TOML) && path.startsWith(Constants.SOURCE);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

package fi.helsinki.cs.tmc.langs.rust.util;

import java.nio.file.Path;
import java.nio.file.Paths;

public class Constants {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be package private and final? Also consider renaming this to CargoConstants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cannot make it package private because it's used outside of util. Also I prefer not adding qualifiers to names which are apparent from the package (Package + class name was supposed to be the unique identifier in Java).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like adding any more public classes (we need to more aggressively start changing structure so that we can manage with package visible classes.)

However for now this is fine.

public static final Path CARGO_TOML = Paths.get("Cargo.toml");
public static final Path SOURCE = Paths.get("src");
}
Loading