Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
162 changes: 145 additions & 17 deletions use-gui/src/it/java/org/tzi/use/main/shell/ShellIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.github.difflib.text.DiffRow;
import com.github.difflib.text.DiffRowGenerator;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.io.TempDir;
import org.tzi.use.config.Options;
import org.tzi.use.main.gui.Main;
import org.tzi.use.util.USEWriter;
Expand All @@ -24,6 +26,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;

/**
Expand Down Expand Up @@ -195,6 +199,99 @@ private void writeToFile(List<String> data, Path file) {
}
}

/**
* Issue #32: the generated command file must always end with an
* {@code exit} command so USE terminates even if the {@code .in} file does
* not contain an explicit {@code exit}.
*/
@Test
public void generatedCommandFileAlwaysEndsWithExit(@TempDir Path dir) throws IOException {
Path inFile = dir.resolve("noexit.in");
Files.writeString(inFile, "?1 + 1" + System.lineSeparator() + "*-> 2 : Integer" + System.lineSeparator());
Path cmdFile = dir.resolve("noexit.in.cmd");

createCommandFile(inFile, cmdFile);

List<String> cmdLines = readNonBlankLines(cmdFile);
assertEquals("exit", cmdLines.get(cmdLines.size() - 1),
"The generated command file must always end with an exit command.");
}

/**
* The appended {@code exit} is echoed by USE, so it must be part of the
* expected output for an {@code .in} file that did not terminate itself.
*/
@Test
public void appendedExitIsAddedToExpectedOutput(@TempDir Path dir) throws IOException {
Path inFile = dir.resolve("noexit.in");
Files.writeString(inFile, "?1 + 1" + System.lineSeparator() + "*-> 2 : Integer" + System.lineSeparator());
Path cmdFile = dir.resolve("noexit.in.cmd");

List<String> expected = createCommandFile(inFile, cmdFile);

assertEquals("exit", expected.get(expected.size() - 1),
"The appended exit must be part of the expected output.");
}

/**
* Tests that load an invalid model make USE exit by itself before the
* command file is read. Such tests are marked with {@code #expected exit}
* and must not expect the (never executed) appended exit.
*/
@Test
public void selfExitingTestsDoNotExpectTheAppendedExit(@TempDir Path dir) throws IOException {
Path inFile = dir.resolve("selfexit.in");
Files.writeString(inFile, "#expected exit" + System.lineSeparator()
+ "?1 + 1" + System.lineSeparator() + "*-> 2 : Integer" + System.lineSeparator());
Path cmdFile = dir.resolve("selfexit.in.cmd");

List<String> expected = createCommandFile(inFile, cmdFile);

assertFalse(expected.contains("exit"),
"A test marked with '#expected exit' must not expect the appended exit.");
}

/**
* If the {@code .in} file already ends with an explicit {@code exit}, the
* appended exit is never executed, so {@code exit} must appear only once in
* the expected output.
*/
@Test
public void exitIsNotDuplicatedWhenInFileAlreadyEndsWithExit(@TempDir Path dir) throws IOException {
Path inFile = dir.resolve("withexit.in");
Files.writeString(inFile, "?1 + 1" + System.lineSeparator() + "*-> 2 : Integer"
+ System.lineSeparator() + "exit" + System.lineSeparator());
Path cmdFile = dir.resolve("withexit.in.cmd");

List<String> expected = createCommandFile(inFile, cmdFile);

assertEquals(1, expected.stream().filter("exit"::equals).count(),
"exit must appear exactly once in the expected output.");
}

/**
* The {@code quit} (and {@code q}) alias also terminates USE, so an .in file
* ending with it must not expect the (never executed) appended exit.
*/
@Test
public void quitTerminatedTestsDoNotExpectTheAppendedExit(@TempDir Path dir) throws IOException {
Path inFile = dir.resolve("withquit.in");
Files.writeString(inFile, "?1 + 1" + System.lineSeparator() + "*-> 2 : Integer"
+ System.lineSeparator() + "quit" + System.lineSeparator());
Path cmdFile = dir.resolve("withquit.in.cmd");

List<String> expected = createCommandFile(inFile, cmdFile);

assertFalse(expected.contains("exit"),
"A test that terminates with quit must not expect the appended exit.");
}

private List<String> readNonBlankLines(Path file) throws IOException {
return Files.readAllLines(file, StandardCharsets.UTF_8).stream()
.filter(line -> !line.isBlank())
.collect(Collectors.toList());
}

/**
* Creates a USE-command file at the position located by the path {@code cmdFile}.
* The file contains all commands that are specified in the {@code inFile}.
Expand All @@ -207,47 +304,78 @@ private void writeToFile(List<String> data, Path file) {
private List<String> createCommandFile(Path inFile, Path cmdFile) {
List<String> expectedOutput = new LinkedList<>();

// Whether USE is expected to terminate by itself before the command
// file is read (e.g. because the test loads an invalid model). Such
// tests are marked with a '#expected exit' comment.
boolean selfExits = false;
// The last command written to the command file. Used to avoid adding a
// duplicate exit to the expected output if the .in file already ends
// with an explicit exit.
String lastCommand = null;

// Build USE command file and build expected output
try (
Stream<String> linesStream = Files.lines(inFile, StandardCharsets.UTF_8);
FileWriter cmdWriter = new FileWriter(cmdFile.toFile(), StandardCharsets.UTF_8, false)
) {

linesStream.forEach(inputLine -> {
for (String inputLine : Files.readAllLines(inFile, StandardCharsets.UTF_8)) {

// Ignore empty lines in expected, since they are also suppressed in the actual output
if (inputLine.isBlank())
return;
continue;

if ((inputLine.startsWith("*") || inputLine.startsWith("#"))
&& inputLine.substring(1).isBlank()) {
return;
continue;
}

if (inputLine.startsWith("*")) {
// Input line minus prefix(*) is expected output
expectedOutput.add(inputLine.substring(1).trim());
} else if (!inputLine.startsWith("#")) { // Not a comment
try {
cmdWriter.write(inputLine);
cmdWriter.write(System.lineSeparator());

// Multi-line commands (backslash and dot) are ignored
if (!inputLine.matches("^[\\\\.]$")) {
expectedOutput.add(inputLine);
}
} catch (IOException e1) {
fail("Could not write USE command file for test!", e1);
} else if (inputLine.startsWith("#")) {
// Comment line. The '#expected exit' marker denotes tests in
// which USE exits by itself (e.g. on an invalid model).
if (inputLine.substring(1).trim().equals("expected exit")) {
selfExits = true;
}
} else { // A command
cmdWriter.write(inputLine);
cmdWriter.write(System.lineSeparator());

// Multi-line commands (backslash and dot) are ignored
if (!inputLine.matches("^[\\\\.]$")) {
expectedOutput.add(inputLine);
}
lastCommand = inputLine.trim();
}
});
}

// Issue #32: always terminate the command file with an exit so USE
// does not keep running when an .in file forgot to add one.
cmdWriter.write("exit");
cmdWriter.write(System.lineSeparator());
} catch (IOException e) {
fail("Could not write USE command file for test!", e);
}

// The appended exit is echoed by USE and therefore part of the output,
// but only when it is actually executed: not when USE exits by itself
// before reading the command file (#expected exit), and not when the
// .in file already terminated with its own exit/quit (which runs first).
if (!selfExits && !isExitCommand(lastCommand)) {
expectedOutput.add("exit");
}

return expectedOutput;
}

/**
* Whether the given command terminates USE, i.e., is one of {@code q},
* {@code quit} or {@code exit} (see {@code Shell.processLine}).
*/
private static boolean isExitCommand(String command) {
return "exit".equals(command) || "quit".equals(command) || "q".equals(command);
}

/**
* Executes USE with the given {@code useFile} as the model
* and the {@code cmdFile} to execute commands.
Expand Down
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t053.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#expected exit
*t053.use:6:2: Missing return type for OCL query operation `firstSecond'.
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t073.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#expected exit
*t073.use:55:2: Missing return type for OCL query operation `numOfArgs'.
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t085.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#expected exit
*t085.use:14:2: Missing return type for OCL query operation `connectedPlusAux'.
*t085.use:12:4: Undefined operation named `connectedPlusAux' in expression `Town.connectedPlusAux(Set(Town))'.
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t088.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#expected exit
*t088.use:14:2: Missing return type for OCL query operation `connectedPlusAux'.
*t088.use:12:4: Undefined operation named `connectedPlusAux' in expression `Town.connectedPlusAux(Set(Town))'.
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t098.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#expected exit
*t098.use:88:6: Class `LoyaltyAccount' already contains an operation named `isEmpty'.
*t098.use:19:16: Expression `(result = self.partners->collect($e : ProgramPartner | $e.deliveredServices))' can never evaluate to true because `Set(Service)' and `Bag(Service)' are unrelated.
*You can change this check using the -extendedTypeSystemChecks switch.
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t104.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#expected exit
*t104.use:9:12: In association `A': Model already contains a class `A'.
3 changes: 2 additions & 1 deletion use-gui/src/it/resources/testfiles/shell/t126.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
!create a : A
!create b : B
!insert (a, b) into C
check
*checking structure...
*checking invariants...
*checked 0 invariants, 0 failures.
*checked 0 invariants, 0 failures.
1 change: 1 addition & 0 deletions use-gui/src/it/resources/testfiles/shell/t127.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

!create a : A
!create b : B
check
*checking structure...
*checking invariants...
*checking invariant (1) `A::inv1': OK.
Expand Down
3 changes: 2 additions & 1 deletion use-gui/src/it/resources/testfiles/shell/t128.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
!set b.b := C(1,1)
? a.a = b.b
*-> true : Boolean
check
*checking structure...
*checking invariants...
*checked 0 invariants, 0 failures.
*checked 0 invariants, 0 failures.
3 changes: 2 additions & 1 deletion use-gui/src/it/resources/testfiles/shell/t129.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
!create b : t129_import#B
? b
*-> b : B
check
*checking structure...
*checking invariants...
*checked 0 invariants, 0 failures.
*checked 0 invariants, 0 failures.
3 changes: 2 additions & 1 deletion use-gui/src/it/resources/testfiles/shell/t130.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
!set b.b := t130_import2#C(1,1)
? b.b
*-> C{x=1, y=1} : C
check
*checking structure...
*checking invariants...
*checked 0 invariants, 0 failures.
*checked 0 invariants, 0 failures.
3 changes: 2 additions & 1 deletion use-gui/src/it/resources/testfiles/shell/t131.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
!set a.c := t131_import#C::H
? a.c
*-> C::H : C
check
*checking structure...
*checking invariants...
*checked 0 invariants, 0 failures.
*checked 0 invariants, 0 failures.
3 changes: 2 additions & 1 deletion use-gui/src/it/resources/testfiles/shell/t132.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
!set a.b := B(1,1)
*<input>:1:0: generation of expression `B(1,1)' failed, with following error:
*<input>:1:11: Undefined operation `B'.
check
*checking structure...
*checking invariants...
*checked 0 invariants, 0 failures.
*checked 0 invariants, 0 failures.