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

--json and --json-file CLI args add to lfc #1686

Merged
merged 6 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ subprojects {
implementation group: 'com.google.inject', name: 'guice', version: guiceVersion
// https://picocli.info/
implementation group: 'info.picocli', name: 'picocli', version: picocliVersion
}
dependencies {
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation group: 'com.google.code.gson', name: 'gson', version: gsonVersion
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: kotlinVersion
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlinVersion
}
Expand Down
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ group=org.lflang
version=0.4.1-SNAPSHOT

[versions]
picocliVersion=4.7.0
googleJavaFormatVersion=1.15.0
guiceVersion=5.1.0
gsonVersion=2.10.1
jacocoVersion=0.8.7
jupiterVersion=5.8.2
jUnitPlatformVersion=1.8.2
Expand All @@ -15,6 +15,7 @@ kotlinVersion=1.6.20
lsp4jVersion=0.14.0
mwe2LaunchVersion=2.12.2
openTest4jVersion=1.2.0
picocliVersion=4.7.0
resourcesVersion=3.16.0
shadowJarVersion=7.1.2
spotlessVersion=6.11.0
Expand Down
114 changes: 95 additions & 19 deletions org.lflang.tests/src/org/lflang/tests/cli/LfcCliTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,29 @@ public class LfcCliTest {
}
""";

static final String JSON_STRING = """
{
"src": "src/File.lf",
"out": "src",
"properties": {
"build-type": "Release",
"clean": true,
"target-compiler": "gcc",
"external-runtime-path": "src",
"federated": true,
"logging": "info",
"lint": true,
"no-compile": true,
"quiet": true,
"rti": "path/to/rti",
"runtime-version": "rs",
"scheduler": "GEDF_NP",
"threading": false,
"workers": "1"
}
}
""";

@Test
public void testHelpArg() {
lfcTester.run("--help", "--version")
Expand All @@ -72,6 +95,30 @@ public void testHelpArg() {
});
}

@Test
public void testMutuallyExclusiveCliArgs() {
lfcTester.run("File.lf", "--json", JSON_STRING)
.verify(result -> {
result.checkStdErr(containsString(
"are mutually exclusive (specify only one)"));
result.checkFailed();
});

lfcTester.run("File.lf", "--json-file", "test.json")
.verify(result -> {
result.checkStdErr(containsString(
"are mutually exclusive (specify only one)"));
result.checkFailed();
});

lfcTester.run("--json", JSON_STRING, "--json-file", "test.json")
.verify(result -> {
result.checkStdErr(containsString(
"are mutually exclusive (specify only one)"));
result.checkFailed();
});
}

@Test
public void testVersion() {
lfcTester.run("--version")
Expand Down Expand Up @@ -163,6 +210,31 @@ public void testGenInSrcDir(@TempDir Path tempDir) throws IOException {
});
}

// Helper method for comparing argument values in tests testGeneratorArgs,
// testGeneratorArgsJsonString and testGeneratorArgsJsonFile.
public void verifyGeneratorArgs(Path tempDir, String[] args) {
LfcOneShotTestFixture fixture = new LfcOneShotTestFixture();

fixture.run(tempDir, args)
.verify(result -> {
// Don't validate execution because args are dummy args.
Properties properties = fixture.lfc.getGeneratorArgs();
assertEquals(properties.getProperty(BuildParm.BUILD_TYPE.getKey()), "Release");
assertEquals(properties.getProperty(BuildParm.CLEAN.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.TARGET_COMPILER.getKey()), "gcc");
assertEquals(properties.getProperty(BuildParm.EXTERNAL_RUNTIME_PATH.getKey()), "src");
assertEquals(properties.getProperty(BuildParm.LOGGING.getKey()), "info");
assertEquals(properties.getProperty(BuildParm.LINT.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.NO_COMPILE.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.QUIET.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.RTI.getKey()), "path/to/rti");
assertEquals(properties.getProperty(BuildParm.RUNTIME_VERSION.getKey()), "rs");
assertEquals(properties.getProperty(BuildParm.SCHEDULER.getKey()), "GEDF_NP");
assertEquals(properties.getProperty(BuildParm.THREADING.getKey()), "false");
assertEquals(properties.getProperty(BuildParm.WORKERS.getKey()), "1");
});
}

@Test
public void testGeneratorArgs(@TempDir Path tempDir)
throws IOException {
Expand All @@ -188,26 +260,30 @@ public void testGeneratorArgs(@TempDir Path tempDir)
"--threading", "false",
"--workers", "1",
};
LfcOneShotTestFixture fixture = new LfcOneShotTestFixture();
verifyGeneratorArgs(tempDir, args);
}

fixture.run(tempDir, args)
.verify(result -> {
// Don't validate execution because args are dummy args.
Properties properties = fixture.lfc.getGeneratorArgs();
assertEquals(properties.getProperty(BuildParm.BUILD_TYPE.getKey()), "Release");
assertEquals(properties.getProperty(BuildParm.CLEAN.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.TARGET_COMPILER.getKey()), "gcc");
assertEquals(properties.getProperty(BuildParm.EXTERNAL_RUNTIME_PATH.getKey()), "src");
assertEquals(properties.getProperty(BuildParm.LOGGING.getKey()), "info");
assertEquals(properties.getProperty(BuildParm.LINT.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.NO_COMPILE.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.QUIET.getKey()), "true");
assertEquals(properties.getProperty(BuildParm.RTI.getKey()), "path/to/rti");
assertEquals(properties.getProperty(BuildParm.RUNTIME_VERSION.getKey()), "rs");
assertEquals(properties.getProperty(BuildParm.SCHEDULER.getKey()), "GEDF_NP");
assertEquals(properties.getProperty(BuildParm.THREADING.getKey()), "false");
assertEquals(properties.getProperty(BuildParm.WORKERS.getKey()), "1");
});
@Test
public void testGeneratorArgsJsonString(@TempDir Path tempDir)
throws IOException {
TempDirBuilder dir = dirBuilder(tempDir);
dir.file("src/File.lf", LF_PYTHON_FILE);
dir.mkdirs("path//to/rti");
lhstrh marked this conversation as resolved.
Show resolved Hide resolved

String[] args = {"--json", JSON_STRING};
verifyGeneratorArgs(tempDir, args);
}

@Test
public void testGeneratorArgsJsonFile(@TempDir Path tempDir)
throws IOException {
TempDirBuilder dir = dirBuilder(tempDir);
dir.file("src/File.lf", LF_PYTHON_FILE);
dir.file("src/test.json", JSON_STRING);
dir.mkdirs("path//to/rti");
lhstrh marked this conversation as resolved.
Show resolved Hide resolved

String[] args = {"--json-file", "src/test.json"};
verifyGeneratorArgs(tempDir, args);
}

static class LfcTestFixture extends CliToolTestFixture {
Expand Down
142 changes: 128 additions & 14 deletions org.lflang/src/org/lflang/cli/CliBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Spec;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
Expand All @@ -23,6 +33,10 @@
import org.lflang.LFRuntimeModule;
import org.lflang.LFStandaloneSetup;
import org.lflang.util.FileUtil;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonParseException;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
Expand All @@ -36,22 +50,42 @@
* @author Atharva Patil
*/
public abstract class CliBase implements Runnable {
/**
* Models a command specification, including the options, positional
* parameters and subcommands supported by the command.
*/
@Spec CommandSpec spec;

/**
* Options and parameters present in both Lfc and Lff.
*/
@Parameters(
arity = "1..",
paramLabel = "FILES",
description = "Paths of the files to run Lingua Franca programs on.")
protected List<Path> files;
static class MutuallyExclusive {
@Parameters(
arity = "1..",
paramLabel = "FILES",
description = "Paths of the files to run Lingua Franca programs on.")
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
protected List<Path> files;

@Option(
names="--json",
description="JSON object containing CLI arguments.")
private String jsonString;

@Option(
names="--json-file",
description="JSON file containing CLI arguments.")
private Path jsonFile;
}

@Option(
names = {"-o", "--output-path"},
defaultValue = "",
fallbackValue = "",
description = "Specify the root output directory.")
private Path outputPath;
@ArgGroup(exclusive = true, multiplicity = "1")
MutuallyExclusive topLevelArg;

@Option(
names = {"-o", "--output-path"},
defaultValue = "",
fallbackValue = "",
description = "Specify the root output directory.")
private Path outputPath;

/**
* Used to collect all errors that happen during validation/generation.
Expand Down Expand Up @@ -111,9 +145,39 @@ public void doExecute(Io io, String[] args) {
/**
* The entrypoint of Picocli applications - the first method called when
* CliBase, which implements the Runnable interface, is instantiated.
* Lfc and Lff have their own specific implementations for this method.
*/
public abstract void run();
public void run() {
// If args are given in a json file, store its contents in jsonString.
if (topLevelArg.jsonFile != null) {
try {
topLevelArg.jsonString = new String(Files.readAllBytes(
io.getWd().resolve(topLevelArg.jsonFile)));
} catch (IOException e) {
reporter.printFatalErrorAndExit(
"No such file: " + topLevelArg.jsonFile);
}
}
// If args are given in a json string, (1) unpack them into an args
// array, and (2) call cmd.execute on them, which assigns them to their
// correct instance variables, then (3) recurses into run().
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
if (topLevelArg.jsonString != null) {
// Unpack args from json string.
String[] args = jsonStringToArgs(topLevelArg.jsonString);
// Execute application on unpacked args.
CommandLine cmd = spec.commandLine();
int exitCode = cmd.execute(args);
io.callSystemExit(exitCode);
// If args are already unpacked, invoke tool-specific logic.
} else {
runTool();
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
}
}

/*
* The entrypoint of tool-specific logic.
* Lfc and Lff have their own specific implementations for this method.
*/
public abstract void runTool();
lhstrh marked this conversation as resolved.
Show resolved Hide resolved

public static Injector getInjector(String toolName, Io io) {
final ReportingBackend reporter
Expand All @@ -139,7 +203,7 @@ protected Path toAbsolutePath(Path other) {
* @return Validated input paths.
*/
protected List<Path> getInputPaths() {
List<Path> paths = files.stream()
List<Path> paths = topLevelArg.files.stream()
.map(io.getWd()::resolve)
.collect(Collectors.toList());

Expand Down Expand Up @@ -252,4 +316,54 @@ public Resource getResource(Path path) {
}
}

private String[] jsonStringToArgs(String jsonString) {
ArrayList<String> argsList = new ArrayList<>();
JsonObject jsonObject = new JsonObject();

// Parse JSON string and get top-level JSON object.
try {
jsonObject = JsonParser.parseString(jsonString).getAsJsonObject();
} catch (JsonParseException e) {
reporter.printFatalErrorAndExit(
"Invalid JSON string:\n" + jsonString);
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
}
// Append input paths.
JsonElement src = jsonObject.get("src");
if (src == null) {
reporter.printFatalErrorAndExit(
"JSON Parse Exception: field \"src\" not found.");
}
argsList.add(src.getAsString());
// Append output path if given.
JsonElement out = jsonObject.get("out");
if (out != null) {
argsList.add("--output-path");
argsList.add(out.getAsString());
}

// If there are no other properties, return args array.
JsonElement properties = jsonObject.get("properties");
if (properties != null) {
// Get the remaining properties.
Set<Entry<String, JsonElement>> entrySet = properties
.getAsJsonObject()
.entrySet();
// Append the remaining properties to the args array.
for(Entry<String,JsonElement> entry : entrySet) {
String property = entry.getKey();
String value = entry.getValue().getAsString();

// Append option.
argsList.add("--" + property);
// Append argument for non-boolean options.
if (value != "true" || property == "threading") {
argsList.add(value);
}
}
}

// Return as String[].
String[] args = argsList.toArray(new String[argsList.size()]);
return args;
}
}
2 changes: 1 addition & 1 deletion org.lflang/src/org/lflang/cli/Lfc.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public static void main(Io io, final String... args) {
* Load the resource, validate it, and, invoke the code generator.
*/
@Override
public void run() {
public void runTool() {
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
List<Path> paths = getInputPaths();
final Path outputRoot = getOutputRoot();
// Hard code the props based on the options we want.
Expand Down
2 changes: 1 addition & 1 deletion org.lflang/src/org/lflang/cli/Lff.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public static void main(Io io, final String... args) {
* Validates all paths and invokes the formatter on the input paths.
*/
@Override
public void run() {
public void runTool() {
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
List<Path> paths = getInputPaths();
final Path outputRoot = getOutputRoot();

Expand Down