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

refactor test framework to support docker tests #750

Merged
merged 31 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
af0fb53
update docker file generation and test
housengw Nov 11, 2021
43b67b5
add first docker test
housengw Nov 11, 2021
c6d303e
add docker test option
housengw Nov 12, 2021
a445849
pass non federated docker build tests
housengw Nov 14, 2021
1404ce9
refactor test framework code
housengw Nov 14, 2021
b5fd170
add more docker tests
housengw Nov 14, 2021
2a33536
update comment
housengw Nov 14, 2021
c7869c4
add docker test for federated execution
housengw Nov 15, 2021
e6f21c2
add comments
housengw Nov 15, 2021
bbc3b05
remove docker tests for macos
housengw Nov 15, 2021
d18284f
update tests
housengw Nov 15, 2021
bb5e240
refactor defaultExcluded
housengw Nov 15, 2021
e907f1a
refactor ccpp test
housengw Nov 15, 2021
97b0b9c
fix minor bug in test exclusion logic
housengw Nov 15, 2021
b7a1f8a
add more tests
housengw Nov 15, 2021
ed0026d
redisable macos docker tests
housengw Nov 15, 2021
ac22380
Merge branch 'master' of github.com:icyphy/lingua-franca into docker-…
housengw Nov 16, 2021
233d452
try testing docker on windows and python target
housengw Nov 22, 2021
3612d50
disable mac docker test
housengw Nov 22, 2021
7550cf7
add more docker tests
housengw Nov 22, 2021
55fd4bd
attempt to fix docker tests for python
housengw Nov 23, 2021
59e8ebf
only test docker for Linux
housengw Nov 23, 2021
303695b
use python-slim as base image
housengw Nov 23, 2021
65794a4
check if docker exists
housengw Nov 23, 2021
fc35723
remove awkward folder names
housengw Nov 27, 2021
3f2de1a
Merge branch 'master' of github.com:icyphy/lingua-franca into docker-…
housengw Nov 30, 2021
3b6865f
Apply suggestions from code review
housengw Dec 3, 2021
c79a9b7
update exclusion name
housengw Dec 3, 2021
c1695ce
apply suggestions from code review
housengw Dec 3, 2021
78c99a5
add comment about docker commands
housengw Dec 3, 2021
9068f86
only enabl docker test on Linux
housengw Dec 3, 2021
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
26 changes: 26 additions & 0 deletions org.lflang.tests/src/org/lflang/tests/AbstractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ protected boolean supportsGenericTypes() {
return false;
}

/**
* Whether to enable {@link #runDockerNonfederatedTests()} and {@link #runDockerFederatedTests()}.
*/
protected boolean supportsDockerOption() {
return false;
}


@Test
public void runExampleTests() {
Expand Down Expand Up @@ -145,6 +152,25 @@ public void runFederatedTests() {
false);
}

@Test
housengw marked this conversation as resolved.
Show resolved Hide resolved
public void runDockerNonfederatedTests() {
Assumptions.assumeTrue(isLinux(), Message.NO_DOCKER_TEST_SUPPORT);
Assumptions.assumeTrue(supportsDockerOption(), Message.NO_DOCKER_SUPPORT);
runTestsForTargets(Message.DESC_DOCKER_NONFEDERATED,
TestCategory.DOCKER_NONFEDERATED::equals, Configurators::noChanges, TestLevel.EXECUTION,
false);
}

@Test
housengw marked this conversation as resolved.
Show resolved Hide resolved
public void runDockerFederatedTests() {
Assumptions.assumeTrue(isLinux(), Message.NO_DOCKER_TEST_SUPPORT);
Assumptions.assumeTrue(supportsDockerOption(), Message.NO_DOCKER_SUPPORT);
Assumptions.assumeTrue(supportsFederatedExecution(), Message.NO_FEDERATION_SUPPORT);
runTestsForTargets(Message.DESC_DOCKER_FEDERATED,
TestCategory.DOCKER_FEDERATED::equals, Configurators::noChanges, TestLevel.EXECUTION,
false);
}


@Test
public void runWithFourThreads() {
Expand Down
24 changes: 12 additions & 12 deletions org.lflang.tests/src/org/lflang/tests/Configurators.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,17 @@ static boolean noChanges(LFTest test) {
* categories.
*/
public static boolean defaultCategoryExclusion(TestCategory category) {
housengw marked this conversation as resolved.
Show resolved Hide resolved
if (category != TestCategory.CONCURRENT && category != TestCategory.FEDERATED &&
category != TestCategory.EXAMPLE) {
// Check if running on Windows
if (TestBase.isWindows()) {
// SERIALIZATION and TARGET tests are currently not
// supported on Windows.
return category != TestCategory.SERIALIZATION &&
category != TestCategory.TARGET;
}
return true;
}
return false;
boolean excluded = false;

// CONCURRENT, FEDERATED, EXAMPLE, DOCKER_FEDERATED, DOCKER_NONFEDERATED are excluded
excluded |= (category == TestCategory.CONCURRENT
|| category == TestCategory.FEDERATED
|| category == TestCategory.EXAMPLE
|| category == TestCategory.DOCKER_FEDERATED
|| category == TestCategory.DOCKER_NONFEDERATED);

// SERIALIZATION and TARGET tests are excluded on Windows.
excluded |= (TestBase.isWindows() && (category == TestCategory.SERIALIZATION || category == TestCategory.TARGET));
return !excluded;
}
}
208 changes: 174 additions & 34 deletions org.lflang.tests/src/org/lflang/tests/TestBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.File;
import java.io.FileWriter;
import java.io.FileFilter;
import java.io.BufferedWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
Expand Down Expand Up @@ -114,6 +122,8 @@ public static class Message {
public static final String ALWAYS_MULTITHREADED = "The reactor-cpp runtime is always multithreaded.";
public static final String NO_THREAD_SUPPORT = "Target does not support the 'threads' property.";
public static final String NO_FEDERATION_SUPPORT = "Target does not support federated execution.";
public static final String NO_DOCKER_SUPPORT = "Target does not support the 'docker' property.";
public static final String NO_DOCKER_TEST_SUPPORT = "Docker tests are only supported on Linux.";
public static final String NO_GENERICS_SUPPORT = "Target does not support generic types.";

/* Descriptions of collections of tests. */
Expand All @@ -125,6 +135,8 @@ public static class Message {
public static final String DESC_MULTIPORT = "Run multiport tests (threads = 0).";
public static final String DESC_AS_FEDERATED = "Run non-federated tests in federated mode.";
public static final String DESC_FEDERATED = "Run federated tests.";
public static final String DESC_DOCKER_NONFEDERATED = "Run docker non-federated tests.";
public static final String DESC_DOCKER_FEDERATED = "Run docker federated tests.";
public static final String DESC_CONCURRENT = "Run concurrent tests.";
public static final String DESC_TARGET_SPECIFIC = "Run target-specific tests (threads = 0)";
public static final String DESC_AS_CCPP = "Running C tests as CCpp.";
Expand Down Expand Up @@ -232,6 +244,24 @@ protected static boolean isWindows() {
return OS.contains("win");
}

/**
* Determine whether the current platform is MacOS.
* @return true if the current platform is MacOS, false otherwise.
*/
protected static boolean isMac() {
String OS = System.getProperty("os.name").toLowerCase();
return OS.contains("mac");
}

/**
* Determine whether the current platform is Linux.
* @return true if the current platform is Linux, false otherwise.
*/
protected static boolean isLinux() {
String OS = System.getProperty("os.name").toLowerCase();
return OS.contains("linux");
}

/**
* End output redirection.
*/
Expand Down Expand Up @@ -426,48 +456,163 @@ private void generateCode(LFTest test) {
* an error code.
*/
private void execute(LFTest test) {
final ProcessBuilder pb = getExecCommand(test);
if (pb == null) {
final List<ProcessBuilder> pbList = getExecCommand(test);
if (pbList.isEmpty()) {
return;
}
try {
var p = pb.start();
var stdout = test.execLog.recordStdOut(p);
var stderr = test.execLog.recordStdErr(p);
if (!p.waitFor(MAX_EXECUTION_TIME_SECONDS, TimeUnit.SECONDS)) {
stdout.interrupt();
stderr.interrupt();
p.destroyForcibly();
test.result = Result.TEST_TIMEOUT;
} else {
if (p.exitValue() == 0) {
test.result = Result.TEST_PASS;
for (ProcessBuilder pb : pbList) {
var p = pb.start();
var stdout = test.execLog.recordStdOut(p);
var stderr = test.execLog.recordStdErr(p);
if (!p.waitFor(MAX_EXECUTION_TIME_SECONDS, TimeUnit.SECONDS)) {
stdout.interrupt();
stderr.interrupt();
p.destroyForcibly();
test.result = Result.TEST_TIMEOUT;
return;
} else {
test.result = Result.TEST_FAIL;
if (p.exitValue() != 0) {
test.result = Result.TEST_FAIL;
return;
}
}
}

} catch (Exception e) {
test.result = Result.TEST_FAIL;
return;
}
test.result = Result.TEST_PASS;
}

/**
* Return a Mapping of federateName -> absolutePathToDockerFile.
* Expects the docker file to be two levels below where the source files are generated (ex. srcGenPath/nameOfFederate/*Dockerfile)
* @param test The test to get the execution command for.
*/
private Map<String, Path> getFederatedDockerFiles(LFTest test) {
Map<String, Path> fedNameToDockerFile = new HashMap<>();
File[] srcGenFiles = test.fileConfig.getSrcGenPath().toFile().listFiles();
for (File srcGenFile : srcGenFiles) {
if (srcGenFile.isDirectory()) {
File[] dockerFile = srcGenFile.listFiles(new FileFilter() {
@Override
public boolean accept(File pathName) {
return pathName.getName().endsWith("Dockerfile");
}
});
assert dockerFile.length == 1;
fedNameToDockerFile.put(srcGenFile.getName(), dockerFile[0].getAbsoluteFile().toPath());
}
}
return fedNameToDockerFile;
}

/**
* Return the content of the bash script used for testing docker option in federated execution.
* @param fedNameToDockerFile A mapping of federateName -> absolutePathToDockerFile
* @param testNetworkName The name of the network used for testing the docker option.
* See https://github.com/lf-lang/lingua-franca/wiki/Containerized-Execution#federated-execution for more details.
*/
private String getDockerRunScript(Map<String, Path> fedNameToDockerFile, String testNetworkName) {
StringBuilder shCode = new StringBuilder();
shCode.append("#!/bin/bash\n");
int n = fedNameToDockerFile.size();
shCode.append("pids=\"\"\n");
shCode.append(String.format("docker run --rm --network=%s --name=rti rti:test -i 1 -n %d &\n", testNetworkName, n));
housengw marked this conversation as resolved.
Show resolved Hide resolved
shCode.append("pids+=\"$!\"\nsleep 3\n");
for (String fedName : fedNameToDockerFile.keySet()) {
Path dockerFile = fedNameToDockerFile.get(fedName);
shCode.append(String.format("docker run --rm --network=%s %s:test -i 1 &\n", testNetworkName, fedName));
shCode.append("pids+=\" $!\"\n");
}
shCode.append("for p in $pids; do\n");
shCode.append(" if wait $p; then\n");
shCode.append(" :\n");
shCode.append(" else\n");
shCode.append(" exit 1\n");
shCode.append(" fi\n");
shCode.append("done\n");
return shCode.toString();
}

/**
* Return a list of ProcessBuilders used to test the docker option under non-federated execution.
* @param test The test to get the execution command for.
*/
private List<ProcessBuilder> getNonfederatedDockerExecCommand(LFTest test) {
var srcGenPath = test.fileConfig.getSrcGenPath();
var dockerPath = srcGenPath.resolve(test.fileConfig.name + ".Dockerfile");
return Arrays.asList(new ProcessBuilder("docker", "build", "-t", "lingua_franca:test", "-f", dockerPath.toString(), srcGenPath.toString()),
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
new ProcessBuilder("docker", "run", "--rm", "lingua_franca:test"),
new ProcessBuilder("docker", "image", "rm", "lingua_franca:test"));
}

/**
* Return a list of ProcessBuilders used to test the docker option under federated execution.
* @param test The test to get the execution command for.
*/
private List<ProcessBuilder> getFederatedDockerExecCommand(LFTest test) {
var rtiPath = test.fileConfig.getSrcGenBasePath().resolve("RTI");
housengw marked this conversation as resolved.
Show resolved Hide resolved
var rtiDockerPath = rtiPath.resolve("rti.Dockerfile");
Map<String, Path> fedNameToDockerFile = getFederatedDockerFiles(test);
try {
File testScript = File.createTempFile("dockertest", null);
housengw marked this conversation as resolved.
Show resolved Hide resolved
testScript.deleteOnExit();
if (!testScript.setExecutable(true)) {
throw new IOException("Failed to make test script executable");
}
FileWriter fileWriter = new FileWriter(testScript.getAbsoluteFile(), true);
String testNetworkName = "linguaFrancaTestNetwork";
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(getDockerRunScript(fedNameToDockerFile, testNetworkName));
bufferedWriter.close();
List<ProcessBuilder> execCommands = new ArrayList<>();
execCommands.add(new ProcessBuilder("docker", "network", "create", testNetworkName));
execCommands.add(new ProcessBuilder("docker", "build", "-t", "rti:test", "-f", rtiDockerPath.toString(), rtiPath.toString()));
for (String fedName : fedNameToDockerFile.keySet()) {
Path dockerFile = fedNameToDockerFile.get(fedName);
execCommands.add(new ProcessBuilder("docker", "build", "-t", fedName + ":test", "-f", dockerFile.toString(), dockerFile.getParent().toString()));
}
execCommands.add(new ProcessBuilder(testScript.getAbsolutePath()));
execCommands.add(new ProcessBuilder("docker", "image", "rm", "rti:test"));
for (String fedName : fedNameToDockerFile.keySet()) {
Path dockerFile = fedNameToDockerFile.get(fedName);
execCommands.add(new ProcessBuilder("docker", "image", "rm", fedName + ":test"));
}
execCommands.add(new ProcessBuilder("docker", "network", "rm", testNetworkName));
return execCommands;
} catch (IOException e) {
return Arrays.asList(new ProcessBuilder("exit", "1"));
}
}

/**
* Return a preconfigured ProcessBuilder for the command
* Return a list of preconfigured ProcessBuilder(s) for the command(s)
* that should be used to execute the test program.
* @param test The test to get the execution command for.
*/
private ProcessBuilder getExecCommand(LFTest test) {
private List<ProcessBuilder> getExecCommand(LFTest test) {
final var nameWithExtension = test.srcFile.getFileName().toString();
final var nameOnly = nameWithExtension.substring(0, nameWithExtension.lastIndexOf('.'));

var srcGenPath = test.fileConfig.getSrcGenPath();
var parentDirName = srcGenPath.getParent().getFileName().toString();
// special case to test docker file generation
if (parentDirName.equalsIgnoreCase(TestCategory.DOCKER_NONFEDERATED.name())) {
return getNonfederatedDockerExecCommand(test);
} else if (parentDirName.equalsIgnoreCase(TestCategory.DOCKER_FEDERATED.name())) {
return getFederatedDockerExecCommand(test);
}

var binPath = test.fileConfig.binPath;
var binaryName = nameOnly;

switch (test.target) {
case C:
case CPP:
case Rust:
case CCPP: {
var binPath = test.fileConfig.binPath;
var binaryName = nameOnly;
if (test.target == Target.Rust) {
// rust binaries uses snake_case
binaryName = StringUtil.camelToSnakeCase(binaryName);
Expand All @@ -482,53 +627,48 @@ private ProcessBuilder getExecCommand(LFTest test) {
// Running the command as .\binary.exe does not work on Windows for
// some reason... Thus we simply pass the full path here, which
// should work across all platforms
return new ProcessBuilder(fullPath.toString()).directory(binPath.toFile());
return Arrays.asList(new ProcessBuilder(fullPath.toString()).directory(binPath.toFile()));
} else {
test.issues.append(fullPath).append(": No such file or directory.").append(System.lineSeparator());
test.result = Result.NO_EXEC_FAIL;
return null;
return new ArrayList<>();
}
}
case Python: {
var binPath = test.fileConfig.binPath;
var binaryName = nameOnly;
var fullPath = binPath.resolve(binaryName);
if (Files.exists(fullPath)) {
// If execution script exists, run it.
return new ProcessBuilder(fullPath.toString()).directory(binPath.toFile());
return Arrays.asList(new ProcessBuilder(fullPath.toString()).directory(binPath.toFile()));
}
var srcGen = test.fileConfig.getSrcGenPath();
fullPath = srcGen.resolve(nameOnly + ".py");
fullPath = srcGenPath.resolve(nameOnly + ".py");
if (Files.exists(fullPath)) {
return new ProcessBuilder("python3", fullPath.getFileName().toString())
.directory(srcGen.toFile());
return Arrays.asList(new ProcessBuilder("python3", fullPath.getFileName().toString())
.directory(srcGenPath.toFile()));
} else {
test.result = Result.NO_EXEC_FAIL;
test.issues.append("File: ").append(fullPath).append(System.lineSeparator());
return null;
return new ArrayList<>();
}
}
case TS: {
var binPath = test.fileConfig.binPath;
var binaryName = nameOnly;
// Adjust binary extension if running on Window
if (System.getProperty("os.name").startsWith("Windows")) {
binaryName += ".exe";
}
var fullPath = binPath.resolve(binaryName);
if (Files.exists(fullPath)) {
// If execution script exists, run it.
return new ProcessBuilder(fullPath.toString()).directory(binPath.toFile());
return Arrays.asList(new ProcessBuilder(fullPath.toString()).directory(binPath.toFile()));
}
// If execution script does not exist, run .js directly.
var dist = test.fileConfig.getSrcGenPath().resolve("dist");
var file = dist.resolve(nameOnly + ".js");
if (Files.exists(file)) {
return new ProcessBuilder("node", file.toString());
return Arrays.asList(new ProcessBuilder("node", file.toString()));
} else {
test.result = Result.NO_EXEC_FAIL;
test.issues.append("File: ").append(file).append(System.lineSeparator());
return null;
return new ArrayList<>();
}
}
default:
Expand Down
3 changes: 2 additions & 1 deletion org.lflang.tests/src/org/lflang/tests/TestRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ public enum TestCategory {
FEDERATED(true),

// non-shared tests

DOCKER_NONFEDERATED(false),
DOCKER_FEDERATED(false),
housengw marked this conversation as resolved.
Show resolved Hide resolved
SERIALIZATION(false),
TARGET(false),
EXAMPLE(false),
Expand Down
Loading