From 9ba6192fbf17af562a9d6c778dd775db4e7d1373 Mon Sep 17 00:00:00 2001 From: Nils Loose Date: Thu, 15 Oct 2026 10:28:43 +0000 Subject: [PATCH 1/5] Emit YAML violation witnesses (format 2.3) next to GraphML SV-COMP 2027 requires Java violation witnesses in the YAML format, version 2.3. WitnessCreator now writes witness.yml with one violation_sequence: a function_return waypoint per recorded Verifier.nondet*() value (constraint `\result == `, location at the closing parenthesis of the call) and a final target waypoint at the violating statement. witness.graphml is still written for the validators that consume the legacy format. - New marker fields from the executor: call index on the line, type descriptor and source file (legacy 4-field markers still parse); new [WITNESS_TARGET] marker carrying the uncaught throwable's stack trace, resolved to the innermost frame inside the task sources. - Columns come from the sources: closing `)` of the n-th nondet call on the line, `assert`/`throw`/first token of the target statement. - Literals per type: L suffix only when needed, char literals, f suffix for floats, NaN/Infinity via Float/Double, strings via `\result.equals("...")` with Java escaping. - CLI: --input (inputs as given, listed with sha256 hashes), --property (specification), --out, --producer-version; the legacy positional form keeps working. Templates are found next to the jar. - JUnit tests for literals, markers, source resolution and the writer. Co-Authored-By: Claude Opus 5.5 --- README.md | 30 +- build.gradle | 6 + src/main/java/App.java | 414 +++++++++++++++----- src/main/java/witness/JavaLiterals.java | 160 ++++++++ src/main/java/witness/SourceFiles.java | 203 ++++++++++ src/main/java/witness/TargetMarker.java | 90 +++++ src/main/java/witness/ValueMarker.java | 119 ++++++ src/main/java/witness/YamlWitness.java | 176 +++++++++ src/test/java/AppTest.java | 183 +++++++++ src/test/java/witness/JavaLiteralsTest.java | 77 ++++ src/test/java/witness/MarkerTest.java | 70 ++++ 11 files changed, 1437 insertions(+), 91 deletions(-) create mode 100644 src/main/java/witness/JavaLiterals.java create mode 100644 src/main/java/witness/SourceFiles.java create mode 100644 src/main/java/witness/TargetMarker.java create mode 100644 src/main/java/witness/ValueMarker.java create mode 100644 src/main/java/witness/YamlWitness.java create mode 100644 src/test/java/AppTest.java create mode 100644 src/test/java/witness/JavaLiteralsTest.java create mode 100644 src/test/java/witness/MarkerTest.java diff --git a/README.md b/README.md index bc9cd54..a083f26 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,33 @@ # WitnessCreator -WitnessCreator creates SV-COMP GraphML violation witnesses from SWAT witness -markers. +WitnessCreator creates SV-COMP violation witnesses from SWAT witness markers: + +- `witness.yml` in the [YAML witness format 2.3](https://gitlab.com/sosy-lab/benchmarking/sv-witnesses) + for Java (one `violation_sequence` with a `function_return` waypoint per + `Verifier.nondet*()` value and a final `target` waypoint at the violating statement), +- `witness.graphml` in the legacy GraphML format, for validators that still consume it. + +## Usage + +```sh +java -jar build/libs/WitnessCreator.jar [] \ + [--input ]... [--property ] [--out ] [--producer-version ] +``` + +- ``: base64 of the SWAT output lines carrying `[WITNESS]` (one per nondet value: + `value@@@line@@@class@@@method@@@callIndex@@@typeDesc@@@sourceFile`) and `[WITNESS_TARGET]` + (the uncaught throwable with its stack trace) markers. An empty string yields the default + violation witness without waypoints. +- `--input`: the task's input files or directories exactly as given to the verifier. They are + searched for the sources (`.java`) and listed, with their SHA-256 hashes, in the witness. +- `--property`: the property file, its content becomes the witness' `specification`. +- `--out`: the output directory (default: ``, the legacy positional source directory). +- `--producer-version`: written to `metadata.producer.version`. + +Columns are derived from the sources: a `function_return` waypoint points to the closing +parenthesis of the `callIndex`-th nondet call on its line, the `target` waypoint to the start of +the violating statement (`assert`, `throw`, or the first token of the innermost statement in the +task sources that appears in the stack trace). ## Build diff --git a/build.gradle b/build.gradle index 0429df6..2b5f58d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ repositories { dependencies { implementation 'org.antlr:ST4:4.3.4' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } java { @@ -22,6 +24,10 @@ java { } } +tasks.named('test') { + useJUnitPlatform() +} + application { mainClass = 'App' } diff --git a/src/main/java/App.java b/src/main/java/App.java index 37f7d43..a9e153a 100755 --- a/src/main/java/App.java +++ b/src/main/java/App.java @@ -1,144 +1,381 @@ -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Base64; -import java.util.LinkedList; import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.stringtemplate.v4.ST; import org.stringtemplate.v4.STGroup; import org.stringtemplate.v4.STRawGroupDir; +import witness.JavaLiterals; +import witness.SourceFiles; +import witness.SourceFiles.InputFile; +import witness.TargetMarker; +import witness.ValueMarker; import witness.WitnessAssumption; import witness.WitnessEdge; import witness.WitnessNode; +import witness.YamlWitness; +import witness.YamlWitness.Waypoint; +/** + * Creates SV-COMP violation witnesses from SWAT's witness markers. + * + *
+ * App <markers> [<dir>] [--input <path>]... [--property <file.prp>] [--out <dir>]
+ *     [--producer-version <version>]
+ * 
+ * + *
    + *
  • {@code markers}: base64 of the output lines carrying {@code [WITNESS]} and + * {@code [WITNESS_TARGET]} markers (empty for the default violation witness). + *
  • {@code dir}: legacy positional argument, the directory holding the task sources; also + * the output directory unless {@code --out} is given. + *
  • {@code --input}: the task's input files or directories exactly as given to the verifier, + * used for the source lookup, the witness' {@code input_files} and their hashes. + *
  • {@code --property}: the property file, its content is the witness' specification. + *
+ * + * Writes {@code witness.yml} (format 2.3) and, for the validators still consuming GraphML, + * {@code witness.graphml}. + */ public class App { - public static void main(String[] args) { + private static final String PRODUCER = "SWAT"; + private static final String DEFAULT_SPECIFICATION = "CHECK( init(Main.main()), LTL(G assert) )"; + private static final Pattern NONDET_CALL = Pattern.compile("nondet[A-Za-z0-9_]*\\s*\\("); + private static final Pattern ASSERT_KEYWORD = Pattern.compile("\\bassert\\b"); + private static final Pattern THROW_KEYWORD = Pattern.compile("\\bthrow\\b"); + private final SourceFiles sourceFiles; + private final Path outDir; - String path = args[1]; - System.out.println("source path: " + path); + private App(SourceFiles sourceFiles, Path outDir) { + this.sourceFiles = sourceFiles; + this.outDir = outDir; + } - List witness = new LinkedList<>(); - if(args[0].trim().length() == 0) { - System.out.println("No witness record, assuming default assertion violation"); - (new App()).saveDefaultviolationWitness(path); - } else { - String param = new String(Base64.getDecoder().decode(args[0])); - String[] lines = param.split("\n"); - for (String line : lines) { - line = line.substring(line.indexOf("[WITNESS]") + 10).trim(); - // decode line base64 - byte[] decodedBytes = Base64.getDecoder().decode(line); - line = new String(decodedBytes); - - String[] parts = line.split("@@@"); - if (parts.length != 4) { - System.out.println("Received line:" + line); - throw new RuntimeException("Invalid witness line: " + line); - } + public static void main(String[] args) throws IOException { + List positional = new ArrayList<>(); + List inputs = new ArrayList<>(); + String property = null; + String out = null; + String producerVersion = "unknown"; + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--input" -> inputs.add(args[++i]); + case "--property" -> property = args[++i]; + case "--out" -> out = args[++i]; + case "--producer-version" -> producerVersion = args[++i]; + default -> positional.add(args[i]); + } + } + if (positional.isEmpty()) { + System.err.println( + "Usage: App [] [--input ]... [--property ]" + + " [--out ] [--producer-version ]"); + System.exit(2); + } + String markers = positional.get(0); + String legacyDir = positional.size() > 1 ? positional.get(1) : null; + if (inputs.isEmpty() && legacyDir != null) { + inputs.add(legacyDir); + } + if (out == null) { + out = legacyDir != null ? legacyDir : "."; + } + Path outDir = Paths.get(out); + Files.createDirectories(outDir); + System.out.println("inputs: " + inputs); + System.out.println("output directory: " + outDir); - String value = parts[0].trim(); - int lineNumber = Integer.parseInt(parts[1].trim()); // account for 0-based line numbers - String className = parts[2].trim(); - String description = parts[3].trim(); + SourceFiles sourceFiles = SourceFiles.collect(inputs); + String specification = readSpecification(property); - // Escape XML special characters in scope to avoid XML parse errors - // (e.g., contains angle brackets that break XML parsing) - String scope = "L" + className + ";." + description; - scope = scope.replace("&", "&").replace("<", "<").replace(">", ">"); - witness.add(new WitnessAssumption(value, className + ".java", scope, lineNumber)); - } - (new App()).checkAndSaveWitness(path, witness); + List values = new ArrayList<>(); + TargetMarker target = null; + if (!markers.trim().isEmpty()) { + String decoded = new String(Base64.getDecoder().decode(markers.trim()), StandardCharsets.UTF_8); + for (String line : decoded.split("\n")) { + int idx = line.indexOf("[WITNESS_TARGET]"); + if (idx >= 0) { + target = TargetMarker.parse(decodePayload(line.substring(idx + "[WITNESS_TARGET]".length()))); + continue; + } + idx = line.indexOf("[WITNESS]"); + if (idx >= 0) { + values.add(ValueMarker.parse(decodePayload(line.substring(idx + "[WITNESS]".length())))); + } } } + if (values.isEmpty()) { + System.out.println("No witness values recorded, assuming default violation"); + } + if (target == null) { + System.out.println("No witness target recorded, the witness has no target waypoint"); + } + App app = new App(sourceFiles, outDir); + app.saveYamlWitness(values, target, specification, producerVersion); + app.saveGraphmlWitness(values); + } + private static String decodePayload(String base64) { + return new String(Base64.getDecoder().decode(base64.trim()), StandardCharsets.UTF_8); + } - private void saveDefaultviolationWitness(String path){ - STGroup group = new STRawGroupDir("witnesses", '$','$'); - ST st = group.getInstanceOf("default_violation"); - String result = st.render(); + private static String readSpecification(String property) { + if (property == null) { + System.out.println("No property file given, assuming specification: " + DEFAULT_SPECIFICATION); + return DEFAULT_SPECIFICATION; + } try { - System.out.println("Writing default violation witness to file: " + path + "/" + "witness.graphml"); - Files.write(Paths.get(path + "/" + "witness.graphml"), result.getBytes()); + String spec = Files.readString(Paths.get(property), StandardCharsets.UTF_8).trim(); + if (!spec.isEmpty()) { + return spec; + } } catch (IOException e) { - System.err.println("Error writing witness to file: " + e.getMessage()); + System.out.println("Could not read property file " + property + ": " + e.getMessage()); + } + System.out.println("Assuming specification: " + DEFAULT_SPECIFICATION); + return DEFAULT_SPECIFICATION; + } + + // ------------------------------------------------------------------ + // YAML witness (format 2.3) + // ------------------------------------------------------------------ + + private void saveYamlWitness( + List values, TargetMarker target, String specification, String producerVersion) + throws IOException { + YamlWitness witness = new YamlWitness(PRODUCER, producerVersion, specification, sourceFiles); + + for (ValueMarker value : values) { + Optional file = sourceFiles.resolve(value.getClassName(), value.getSourceFile()); + String fileName; + Integer column = null; + if (file.isPresent()) { + fileName = file.get().getDisplayPath(); + String loc = file.get().getLine(value.getLine()); + int col = loc == null ? -1 : nondetCallColumn(loc, value.getCallIndex()); + if (col > 0) { + column = col; + } else { + System.out.println( + "Could not locate nondet call " + value.getCallIndex() + " in " + fileName + + ":" + value.getLine() + " (" + loc + ")"); + } + } else { + fileName = fallbackFileName(value.getClassName(), value.getSourceFile()); + System.out.println("Could not find the source of " + value.getClassName() + ", using " + fileName); + } + String constraint = JavaLiterals.resultConstraint(value.getValue(), value.getTypeDesc()); + witness.add(Waypoint.functionReturn(constraint, fileName, value.getLine(), column)); + } + + if (target != null) { + Optional targetWaypoint = targetWaypoint(target); + if (targetWaypoint.isPresent()) { + witness.add(targetWaypoint.get()); + } else { + System.out.println("No stack frame of " + target.getThrowableClass() + + " lies in the task sources: " + target.getFrames()); + } + } + + for (Waypoint wp : witness.getWaypoints()) { + System.out.println("Waypoint: " + wp); } + Path file = outDir.resolve("witness.yml"); + System.out.println("Writing violation witness to file: " + file); + Files.writeString(file, witness.render(), StandardCharsets.UTF_8); } - private void checkAndSaveWitness(String path, List witness) { + /** The innermost stack frame inside the task sources becomes the target. */ + private Optional targetWaypoint(TargetMarker target) { + for (TargetMarker.Frame frame : target.getFrames()) { + if (frame.getLine() < 1) { + continue; + } + Optional file = sourceFiles.resolve(frame.getClassName(), frame.getFileName()); + if (file.isEmpty()) { + continue; + } + String loc = file.get().getLine(frame.getLine()); + if (loc == null) { + System.out.println("Frame " + frame + " is outside of " + file.get().getPath()); + continue; + } + int col = statementColumn(loc, target.isAssertionError()); + return Optional.of( + Waypoint.target(file.get().getDisplayPath(), frame.getLine(), col > 0 ? col : null)); + } + return Optional.empty(); + } - List nodes = new ArrayList<>(); - List edges = new ArrayList<>(); + /** + * The 1-based column of the closing parenthesis of the {@code callIndex}-th nondet call in the + * line, -1 if there is none. + */ + static int nondetCallColumn(String line, int callIndex) { + Matcher m = NONDET_CALL.matcher(line); + int found = 0; + while (m.find()) { + if (found++ < callIndex) { + continue; + } + int open = m.end() - 1; + int depth = 0; + for (int i = open; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + return i + 1; + } + } + } + return -1; + } + return -1; + } - int nodeId = 0; - WitnessNode initNode = new WitnessNode(nodeId++); - nodes.add(initNode); - initNode.addData("entry", "true"); - WitnessNode curNode = initNode; + /** + * The 1-based column of the first character of the violating statement on the line: the + * {@code assert} of a failed assertion, the {@code throw} of an explicit throw, otherwise the + * first token that is not a closing brace or an {@code else}. + */ + static int statementColumn(String line, boolean assertion) { + if (assertion) { + Matcher m = ASSERT_KEYWORD.matcher(line); + if (m.find()) { + return m.start() + 1; + } + } + Matcher t = THROW_KEYWORD.matcher(line); + if (t.find()) { + return t.start() + 1; + } + int i = skipWhitespace(line, 0); + while (i < line.length()) { + if (line.charAt(i) == '}' || line.charAt(i) == '{') { + i = skipWhitespace(line, i + 1); + } else if (line.startsWith("else", i) + && (i + 4 == line.length() || !Character.isJavaIdentifierPart(line.charAt(i + 4)))) { + i = skipWhitespace(line, i + 4); + } else { + break; + } + } + return i < line.length() ? i + 1 : -1; + } - for (WitnessAssumption wa : witness) { - String loc = getLineOfCode(path, wa.getClazz(), wa.getLine()); - String assumption = computeAssumption(loc, wa); + private static int skipWhitespace(String line, int i) { + while (i < line.length() && Character.isWhitespace(line.charAt(i))) { + i++; + } + return i; + } - WitnessNode prevNode = curNode; - curNode = new WitnessNode(nodeId++); - nodes.add(curNode); - WitnessEdge edge = new WitnessEdge(prevNode, wa, assumption, curNode); - edges.add(edge); + private static String fallbackFileName(String className, String sourceFile) { + String internal = className.replace('.', '/'); + int slash = internal.lastIndexOf('/'); + String pkgDir = slash < 0 ? "" : internal.substring(0, slash + 1); + if (sourceFile != null && !sourceFile.isEmpty()) { + return pkgDir + sourceFile; } + String simple = internal.substring(slash + 1); + int dollar = simple.indexOf('$'); + return pkgDir + (dollar < 0 ? simple : simple.substring(0, dollar)) + ".java"; + } + + // ------------------------------------------------------------------ + // GraphML witness (legacy format, kept for the GraphML-based validators) + // ------------------------------------------------------------------ + + private void saveGraphmlWitness(List values) { + STGroup group = new STRawGroupDir(templateDir(), '$', '$'); + ST st; + if (values.isEmpty()) { + st = group.getInstanceOf("default_violation"); + } else { + List nodes = new ArrayList<>(); + List edges = new ArrayList<>(); + + int nodeId = 0; + WitnessNode initNode = new WitnessNode(nodeId++); + nodes.add(initNode); + initNode.addData("entry", "true"); + WitnessNode curNode = initNode; + + for (ValueMarker value : values) { + Optional file = sourceFiles.resolve(value.getClassName(), value.getSourceFile()); + String clazz = file.map(InputFile::getRelativePath) + .orElse(fallbackFileName(value.getClassName(), value.getSourceFile())); + String loc = file.map(f -> f.getLine(value.getLine())).orElse(null); + // Escape XML special characters in scope to avoid XML parse errors + // (e.g., contains angle brackets that break XML parsing) + String scope = "L" + value.getClassName() + ";." + value.getMethodDesc(); + scope = scope.replace("&", "&").replace("<", "<").replace(">", ">"); + WitnessAssumption wa = new WitnessAssumption(value.getValue(), clazz, scope, value.getLine()); + String assumption = computeAssumption(loc, wa, value.isString()); - curNode.addData("violation", "true"); + WitnessNode prevNode = curNode; + curNode = new WitnessNode(nodeId++); + nodes.add(curNode); + edges.add(new WitnessEdge(prevNode, wa, assumption, curNode)); + } + curNode.addData("violation", "true"); - STGroup group = new STRawGroupDir("witnesses", '$','$'); - ST st = group.getInstanceOf("witness"); - st.add("nodes", nodes); - st.add("edges", edges); - String result = st.render(); + st = group.getInstanceOf("witness"); + st.add("nodes", nodes); + st.add("edges", edges); + } + Path file = outDir.resolve("witness.graphml"); try { - System.out.println("Writing violation witness to file: " + path + "/" + "witness.graphml"); - Files.write(Paths.get(path + "/" + "witness.graphml"), result.getBytes()); + System.out.println("Writing violation witness to file: " + file); + Files.writeString(file, st.render(), StandardCharsets.UTF_8); } catch (IOException e) { System.out.println("Error writing witness to file: " + e.getMessage()); } } - private String getLineOfCode(String path, String filename, int line) { - String absolutePath = path + "/" + filename; + /** The {@code witnesses} template directory next to the jar, or relative to the cwd. */ + private static String templateDir() { + if (Files.isDirectory(Paths.get("witnesses"))) { + return "witnesses"; + } try { - // Open absolutePath as a stream - InputStream is = Files.newInputStream(Paths.get(absolutePath)); - if (is == null) { - System.out.println("Could not find file: " + absolutePath); - return null; - } - BufferedReader res = new BufferedReader(new InputStreamReader(is)); - String loc = ""; - for (int i = 0; i= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) { + return value; + } + return value + "L"; + } catch (NumberFormatException e) { + return value; + } + } + + private static String floatingConstraint(String value, boolean isFloat) { + String box = isFloat ? "Float" : "Double"; + switch (value) { + case "NaN": + return box + ".isNaN(" + RESULT + ")"; + case "Infinity": + return RESULT + " == " + box + ".POSITIVE_INFINITY"; + case "-Infinity": + return RESULT + " == " + box + ".NEGATIVE_INFINITY"; + default: + break; + } + String literal = value; + try { + // Normalises scientific notation and guarantees a decimal point; BigDecimal has no + // negative zero, so the sign is kept separately + boolean negative = value.startsWith("-"); + literal = new BigDecimal(negative ? value.substring(1) : value).toPlainString(); + if (negative) { + literal = "-" + literal; + } + } catch (NumberFormatException e) { + // keep as is + } + if (!literal.contains(".")) { + literal += ".0"; + } + return RESULT + " == " + literal + (isFloat ? "f" : ""); + } + + /** Char values are recorded as their code point. */ + public static String charLiteral(String codePoint) { + int c; + try { + c = Integer.parseInt(codePoint); + } catch (NumberFormatException e) { + return codePoint; + } + switch (c) { + case '\'': + return "'\\''"; + case '\\': + return "'\\\\'"; + case '\n': + return "'\\n'"; + case '\r': + return "'\\r'"; + case '\t': + return "'\\t'"; + case '\b': + return "'\\b'"; + case '\f': + return "'\\f'"; + default: + break; + } + if (c >= 0x20 && c < 0x7f) { + return "'" + (char) c + "'"; + } + return String.format("'\\u%04x'", c); + } + + public static String stringLiteral(String value) { + StringBuilder sb = new StringBuilder("\""); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + default: + if (c < 0x20 || c == 0x7f) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + return sb.append('"').toString(); + } +} diff --git a/src/main/java/witness/SourceFiles.java b/src/main/java/witness/SourceFiles.java new file mode 100644 index 0000000..bee2b45 --- /dev/null +++ b/src/main/java/witness/SourceFiles.java @@ -0,0 +1,203 @@ +package witness; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * The Java source files of the verification task, as they were given to the verifier (files or + * directories). Resolves classes to their source files and provides line contents and hashes. + */ +public final class SourceFiles { + + /** One input file of the task. */ + public static final class InputFile { + private final String displayPath; + private final Path path; + private final String relativePath; + private List lines; + private String sha256; + + InputFile(String displayPath, Path path, String relativePath) { + this.displayPath = displayPath; + this.path = path; + this.relativePath = relativePath; + } + + /** The path as it is written into the witness (input as given + relative path). */ + public String getDisplayPath() { + return displayPath; + } + + public Path getPath() { + return path; + } + + /** Path relative to the input root it was found in, '/'-separated. */ + public String getRelativePath() { + return relativePath; + } + + public String getFileName() { + return path.getFileName().toString(); + } + + public List getLines() { + if (lines == null) { + try { + lines = Files.readAllLines(path, StandardCharsets.UTF_8); + } catch (IOException e) { + System.out.println("Could not read " + path + ": " + e.getMessage()); + lines = Collections.emptyList(); + } + } + return lines; + } + + /** The 1-based line, or {@code null} if the file does not have it. */ + public String getLine(int lineNumber) { + List l = getLines(); + if (lineNumber < 1 || lineNumber > l.size()) { + return null; + } + return l.get(lineNumber - 1); + } + + public String getSha256() throws IOException { + if (sha256 == null) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(Files.readAllBytes(path)); + StringBuilder sb = new StringBuilder(); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + sha256 = sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + return sha256; + } + } + + private final List files; + + private SourceFiles(List files) { + this.files = Collections.unmodifiableList(files); + } + + /** + * Collects all {@code .java} files of the given inputs. Directories are searched recursively, + * the files are ordered by path inside each input. + */ + public static SourceFiles collect(List inputs) { + List files = new ArrayList<>(); + Set seen = new HashSet<>(); // a file reachable through several inputs is listed once + for (String input : inputs) { + Path root = Paths.get(input); + if (Files.isDirectory(root)) { + String prefix = stripTrailingSlash(input); + try (Stream walk = Files.walk(root)) { + List javaFiles = + walk.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(".java")) + .sorted() + .toList(); + for (Path p : javaFiles) { + if (seen.add(p.toAbsolutePath().normalize())) { + String rel = toSlashes(root.relativize(p).toString()); + files.add(new InputFile(prefix + "/" + rel, p, rel)); + } + } + } catch (IOException e) { + System.out.println("Could not read input directory " + input + ": " + e.getMessage()); + } + } else if (Files.isRegularFile(root)) { + if (root.getFileName().toString().endsWith(".java") + && seen.add(root.toAbsolutePath().normalize())) { + files.add(new InputFile(toSlashes(input), root, root.getFileName().toString())); + } + } else { + System.out.println("Ignoring missing input: " + input); + } + } + return new SourceFiles(files); + } + + public List getFiles() { + return files; + } + + public boolean isEmpty() { + return files.isEmpty(); + } + + /** + * Finds the source file of a class. + * + * @param className JVM internal ({@code pkg/Outer$Inner}) or dotted ({@code pkg.Outer$Inner}) + * class name + * @param sourceFileName the class' SourceFile attribute, may be empty or {@code null} + */ + public Optional resolve(String className, String sourceFileName) { + String internal = className.replace('.', '/'); + int slash = internal.lastIndexOf('/'); + String pkgDir = slash < 0 ? "" : internal.substring(0, slash); + String simple = slash < 0 ? internal : internal.substring(slash + 1); + int dollar = simple.indexOf('$'); + String outer = dollar < 0 ? simple : simple.substring(0, dollar); + + List fileNames = new ArrayList<>(); + if (sourceFileName != null && !sourceFileName.isEmpty()) { + fileNames.add(sourceFileName); + } + fileNames.add(outer + ".java"); + + // Exact package path first, then any path ending with the package path, then by name only + for (String fileName : fileNames) { + String rel = pkgDir.isEmpty() ? fileName : pkgDir + "/" + fileName; + Optional match = find(f -> f.getRelativePath().equals(rel)); + if (match.isEmpty()) { + match = find(f -> f.getRelativePath().endsWith("/" + rel)); + } + if (match.isPresent()) { + return match; + } + } + for (String fileName : fileNames) { + Optional match = find(f -> f.getFileName().equals(fileName)); + if (match.isPresent()) { + return match; + } + } + return Optional.empty(); + } + + private Optional find(java.util.function.Predicate predicate) { + return files.stream().filter(predicate).findFirst(); + } + + private static String stripTrailingSlash(String s) { + String r = toSlashes(s); + while (r.length() > 1 && r.endsWith("/")) { + r = r.substring(0, r.length() - 1); + } + return r; + } + + private static String toSlashes(String s) { + return s.replace('\\', '/'); + } +} diff --git a/src/main/java/witness/TargetMarker.java b/src/main/java/witness/TargetMarker.java new file mode 100644 index 0000000..a74f253 --- /dev/null +++ b/src/main/java/witness/TargetMarker.java @@ -0,0 +1,90 @@ +package witness; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A decoded {@code [WITNESS_TARGET]} marker: the uncaught throwable that ended the violating run + * together with its stack trace (innermost frame first). + * + *

Payload format: {@code throwableClass@@@className@@fileName@@line;className@@fileName@@line;...} + */ +public final class TargetMarker { + private final String throwableClass; + private final List frames; + + public static final class Frame { + private final String className; + private final String fileName; + private final int line; + + Frame(String className, String fileName, int line) { + this.className = className; + this.fileName = fileName; + this.line = line; + } + + /** Fully qualified (dotted) class name. */ + public String getClassName() { + return className; + } + + /** Source file name from the stack trace, empty if unknown. */ + public String getFileName() { + return fileName; + } + + public int getLine() { + return line; + } + + @Override + public String toString() { + return className + "(" + fileName + ":" + line + ")"; + } + } + + private TargetMarker(String throwableClass, List frames) { + this.throwableClass = throwableClass; + this.frames = Collections.unmodifiableList(frames); + } + + public static TargetMarker parse(String payload) { + int idx = payload.indexOf("@@@"); + if (idx < 0) { + throw new IllegalArgumentException("Invalid witness target marker: " + payload); + } + String throwableClass = payload.substring(0, idx).trim(); + List frames = new ArrayList<>(); + String rest = payload.substring(idx + 3).trim(); + if (!rest.isEmpty()) { + for (String frame : rest.split(";")) { + String[] parts = frame.split("@@", -1); + if (parts.length != 3) { + continue; + } + int line; + try { + line = Integer.parseInt(parts[2].trim()); + } catch (NumberFormatException e) { + line = -1; + } + frames.add(new Frame(parts[0].trim(), parts[1].trim(), line)); + } + } + return new TargetMarker(throwableClass, frames); + } + + public String getThrowableClass() { + return throwableClass; + } + + public boolean isAssertionError() { + return "java.lang.AssertionError".equals(throwableClass); + } + + public List getFrames() { + return frames; + } +} diff --git a/src/main/java/witness/ValueMarker.java b/src/main/java/witness/ValueMarker.java new file mode 100644 index 0000000..1abf53d --- /dev/null +++ b/src/main/java/witness/ValueMarker.java @@ -0,0 +1,119 @@ +package witness; + +import java.util.Arrays; +import java.util.Set; + +/** + * A decoded {@code [WITNESS]} marker: one value returned by a {@code Verifier.nondet*()} call. + * + *

Payload format (fields separated by {@code @@@}): + * {@code value@@@line@@@className@@@methodDesc@@@callIndex@@@typeDesc@@@sourceFile}. + * The legacy format {@code value@@@line@@@className@@@methodDesc} is still accepted. Because a + * String value may itself contain the separator, the fields are parsed from the end. + */ +public final class ValueMarker { + private static final String SEPARATOR = "@@@"; + private static final Set TYPE_DESCRIPTORS = + Set.of("Z", "B", "C", "S", "I", "J", "F", "D", "Ljava/lang/String;"); + + private final String value; + private final int line; + private final String className; + private final String methodDesc; + private final int callIndex; + private final String typeDesc; + private final String sourceFile; + + private ValueMarker( + String value, + int line, + String className, + String methodDesc, + int callIndex, + String typeDesc, + String sourceFile) { + this.value = value; + this.line = line; + this.className = className; + this.methodDesc = methodDesc; + this.callIndex = callIndex; + this.typeDesc = typeDesc; + this.sourceFile = sourceFile; + } + + public static ValueMarker parse(String payload) { + String[] parts = payload.split(SEPARATOR, -1); + int n = parts.length; + if (n >= 7 && TYPE_DESCRIPTORS.contains(parts[n - 2].trim()) && isInt(parts[n - 3])) { + return new ValueMarker( + join(parts, n - 6), + Integer.parseInt(parts[n - 6].trim()), + parts[n - 5].trim(), + parts[n - 4].trim(), + Integer.parseInt(parts[n - 3].trim()), + parts[n - 2].trim(), + parts[n - 1].trim()); + } + if (n >= 4 && isInt(parts[n - 3])) { + return new ValueMarker( + join(parts, n - 3), + Integer.parseInt(parts[n - 3].trim()), + parts[n - 2].trim(), + parts[n - 1].trim(), + 0, + null, + ""); + } + throw new IllegalArgumentException("Invalid witness marker: " + payload); + } + + private static String join(String[] parts, int count) { + return String.join(SEPARATOR, Arrays.copyOfRange(parts, 0, count)); + } + + private static boolean isInt(String s) { + try { + Integer.parseInt(s.trim()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** The recorded value as printed by the executor (chars as their code point). */ + public String getValue() { + return value; + } + + public int getLine() { + return line; + } + + /** JVM internal class name of the caller, e.g. {@code pkg/Main$Inner}. */ + public String getClassName() { + return className; + } + + public String getMethodDesc() { + return methodDesc; + } + + /** 0-based index of the call among the nondet calls on the same source line. */ + public int getCallIndex() { + return callIndex; + } + + /** JVM descriptor of the returned type, or {@code null} for legacy markers. */ + public String getTypeDesc() { + return typeDesc; + } + + /** The caller's SourceFile attribute, empty if unknown. */ + public String getSourceFile() { + return sourceFile; + } + + public boolean isString() { + return "Ljava/lang/String;".equals(typeDesc); + } +} diff --git a/src/main/java/witness/YamlWitness.java b/src/main/java/witness/YamlWitness.java new file mode 100644 index 0000000..7298c0f --- /dev/null +++ b/src/main/java/witness/YamlWitness.java @@ -0,0 +1,176 @@ +package witness; + +import java.io.IOException; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * A violation witness in the YAML format, version 2.3, for Java (see + * https://gitlab.com/sosy-lab/benchmarking/sv-witnesses). The witness is a single + * {@code violation_sequence} whose segments each hold one {@code follow} waypoint: one + * {@code function_return} waypoint per recorded {@code Verifier.nondet*()} value and, if the + * violating statement is known, a final {@code target} waypoint. + */ +public final class YamlWitness { + public static final String FORMAT_VERSION = "2.3"; + + /** A follow waypoint. */ + public static final class Waypoint { + private final String type; + private final String constraint; + private final String fileName; + private final int line; + private final Integer column; + + private Waypoint(String type, String constraint, String fileName, int line, Integer column) { + this.type = type; + this.constraint = constraint; + this.fileName = fileName; + this.line = line; + this.column = column; + } + + public static Waypoint functionReturn( + String constraint, String fileName, int line, Integer column) { + return new Waypoint("function_return", constraint, fileName, line, column); + } + + public static Waypoint target(String fileName, int line, Integer column) { + return new Waypoint("target", null, fileName, line, column); + } + + public String getType() { + return type; + } + + @Override + public String toString() { + return type + + " @ " + + fileName + + ":" + + line + + (column == null ? "" : ":" + column) + + (constraint == null ? "" : " [" + constraint + "]"); + } + } + + private final String producerName; + private final String producerVersion; + private final String specification; + private final SourceFiles sourceFiles; + private final List waypoints = new ArrayList<>(); + + public YamlWitness( + String producerName, + String producerVersion, + String specification, + SourceFiles sourceFiles) { + this.producerName = producerName; + this.producerVersion = producerVersion; + this.specification = specification; + this.sourceFiles = sourceFiles; + } + + public void add(Waypoint waypoint) { + waypoints.add(waypoint); + } + + public List getWaypoints() { + return waypoints; + } + + public String render() throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append("- entry_type: \"violation_sequence\"\n"); + sb.append(" metadata:\n"); + sb.append(" format_version: ").append(quote(FORMAT_VERSION)).append('\n'); + sb.append(" uuid: ").append(quote(UUID.randomUUID().toString())).append('\n'); + String time = + ZonedDateTime.now(ZoneOffset.UTC) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")); + sb.append(" creation_time: ").append(quote(time)).append('\n'); + sb.append(" producer:\n"); + sb.append(" name: ").append(quote(producerName)).append('\n'); + sb.append(" version: ").append(quote(producerVersion)).append('\n'); + sb.append(" task:\n"); + if (sourceFiles.isEmpty()) { + sb.append(" input_files: []\n"); + sb.append(" input_file_hashes: {}\n"); + } else { + sb.append(" input_files:\n"); + for (SourceFiles.InputFile f : sourceFiles.getFiles()) { + sb.append(" - ").append(quote(f.getDisplayPath())).append('\n'); + } + sb.append(" input_file_hashes:\n"); + for (SourceFiles.InputFile f : sourceFiles.getFiles()) { + sb.append(" ") + .append(quote(f.getDisplayPath())) + .append(": ") + .append(quote(f.getSha256())) + .append('\n'); + } + } + sb.append(" specification: ").append(quote(specification)).append('\n'); + sb.append(" language: \"Java\"\n"); + if (waypoints.isEmpty()) { + sb.append(" content: []\n"); + return sb.toString(); + } + sb.append(" content:\n"); + for (Waypoint wp : waypoints) { + sb.append(" - segment:\n"); + sb.append(" - waypoint:\n"); + sb.append(" type: ").append(quote(wp.type)).append('\n'); + sb.append(" action: \"follow\"\n"); + if (wp.constraint != null) { + sb.append(" constraint:\n"); + sb.append(" value: ").append(quote(wp.constraint)).append('\n'); + sb.append(" format: \"java_expression\"\n"); + } + sb.append(" location:\n"); + sb.append(" file_name: ").append(quote(wp.fileName)).append('\n'); + sb.append(" line: ").append(wp.line).append('\n'); + if (wp.column != null) { + sb.append(" column: ").append(wp.column).append('\n'); + } + } + return sb.toString(); + } + + /** Renders a YAML double-quoted scalar. */ + static String quote(String s) { + StringBuilder sb = new StringBuilder("\""); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (c < 0x20 || c == 0x7f) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + return sb.append('"').toString(); + } +} diff --git a/src/test/java/AppTest.java b/src/test/java/AppTest.java new file mode 100644 index 0000000..3079d3e --- /dev/null +++ b/src/test/java/AppTest.java @@ -0,0 +1,183 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import witness.SourceFiles; + +class AppTest { + + @Test + void columnOfClosingParenthesis() { + String line = " Boolean input_1_0 = Verifier.nondetBoolean();"; + assertEquals(48, App.nondetCallColumn(line, 0)); + assertEquals(-1, App.nondetCallColumn(line, 1)); + + String two = " if (!chunked && Verifier.nondetInt() > 0 && Verifier.nondetInt() != 0) {"; + assertEquals(')', two.charAt(App.nondetCallColumn(two, 0) - 1)); + assertEquals(')', two.charAt(App.nondetCallColumn(two, 1) - 1)); + assertTrue(App.nondetCallColumn(two, 0) < App.nondetCallColumn(two, 1)); + + String spaced = "x = Verifier.nondetInt ( );"; + assertEquals(26, App.nondetCallColumn(spaced, 0)); + assertEquals(-1, App.nondetCallColumn("no call here", 0)); + assertEquals(-1, App.nondetCallColumn("Verifier.nondetInt(", 0)); + } + + @Test + void columnOfViolatingStatement() { + assertEquals(12, App.statementColumn(" } else assert false;", true)); + assertEquals(5, App.statementColumn(" assert !(output_1 == 1);", true)); + assertEquals(5, App.statementColumn(" assert !(output_1 == 1);", false)); + assertEquals(7, App.statementColumn(" } throw new RuntimeException();", false)); + assertEquals(5, App.statementColumn(" int x = a / b;", false)); + assertEquals(14, App.statementColumn(" } else { int x = a / b; }", false)); + // tabs count as one character, like any other + assertEquals(4, App.statementColumn("\t\t\tx = elsewhere / 0;", false)); + assertEquals(-1, App.statementColumn(" }", false)); + assertEquals(-1, App.statementColumn("", false)); + } + + @Test + void resolvesSources(@TempDir Path dir) throws IOException { + Path task = dir.resolve("task"); + Files.createDirectories(task.resolve("pkg")); + Files.writeString(task.resolve("Main.java"), "class Main {}\nclass Helper {}\n"); + Files.writeString(task.resolve("pkg/Foo.java"), "package pkg;\nclass Foo {}\n"); + Path common = dir.resolve("common"); + Files.createDirectories(common.resolve("org/sosy_lab/sv_benchmarks")); + Files.writeString(common.resolve("org/sosy_lab/sv_benchmarks/Verifier.java"), "package org.sosy_lab.sv_benchmarks;\n"); + + SourceFiles files = SourceFiles.collect(List.of(common + "/", task.toString())); + assertEquals( + List.of( + common + "/org/sosy_lab/sv_benchmarks/Verifier.java", + task + "/Main.java", + task + "/pkg/Foo.java"), + files.getFiles().stream().map(SourceFiles.InputFile::getDisplayPath).toList()); + + assertEquals("Main.java", files.resolve("Main", "Main.java").get().getRelativePath()); + assertEquals("Main.java", files.resolve("Main$1", "").get().getRelativePath()); + assertEquals("Main.java", files.resolve("Helper", "Main.java").get().getRelativePath()); + assertEquals("pkg/Foo.java", files.resolve("pkg/Foo", "Foo.java").get().getRelativePath()); + assertEquals("pkg/Foo.java", files.resolve("pkg.Foo$Inner", "").get().getRelativePath()); + assertEquals( + "org/sosy_lab/sv_benchmarks/Verifier.java", + files.resolve("org.sosy_lab.sv_benchmarks.Verifier", "Verifier.java").get().getRelativePath()); + assertEquals(Optional.empty(), files.resolve("Missing", "")); + assertEquals(64, files.getFiles().get(0).getSha256().length()); + + // A file reachable through several inputs (e.g. a nested common/ dir) is listed once + SourceFiles nested = SourceFiles.collect(List.of(task + "/pkg", task.toString(), task + "/pkg/Foo.java")); + assertEquals( + List.of(task + "/pkg/Foo.java", task + "/Main.java"), + nested.getFiles().stream().map(SourceFiles.InputFile::getDisplayPath).toList()); + } + + @Test + void writesYamlAndGraphmlWitness(@TempDir Path dir) throws IOException { + Path task = dir.resolve("task"); + Files.createDirectories(task); + Files.writeString( + task.resolve("Main.java"), + String.join( + "\n", + "import org.sosy_lab.sv_benchmarks.Verifier;", + "public class Main {", + " public static void main(String[] args) {", + " int x = Verifier.nondetInt();", + " String s = Verifier.nondetString();", + " if (x == 3 && s.equals(\"a\\\"b\")) {", + " assert false;", + " }", + " }", + "}", + "")); + Path prp = dir.resolve("valid-assert.prp"); + Files.writeString(prp, "CHECK( init(Main.main()), LTL(G assert) )\n"); + Path out = dir.resolve("out"); + + String markers = + marker("[WITNESS]", "3@@@4@@@Main@@@main([Ljava/lang/String;)V@@@0@@@I@@@Main.java") + + "\n" + + marker("[WITNESS]", "a\"b@@@5@@@Main@@@main([Ljava/lang/String;)V@@@0@@@Ljava/lang/String;@@@Main.java") + + "\n" + + marker("[WITNESS_TARGET]", "java.lang.AssertionError@@@Main@@Main.java@@7"); + App.main(new String[] { + Base64.getEncoder().encodeToString(markers.getBytes(StandardCharsets.UTF_8)), + "--input", task.toString(), + "--property", prp.toString(), + "--out", out.toString(), + "--producer-version", "test" + }); + + String yaml = Files.readString(out.resolve("witness.yml")); + assertTrue(yaml.startsWith("- entry_type: \"violation_sequence\"\n")); + assertTrue(yaml.contains(" format_version: \"2.3\"\n")); + assertTrue(yaml.contains(" name: \"SWAT\"\n version: \"test\"\n")); + assertTrue(yaml.contains(" - \"" + task + "/Main.java\"\n")); + assertTrue(yaml.contains(" specification: \"CHECK( init(Main.main()), LTL(G assert) )\"\n")); + assertTrue(yaml.contains(" language: \"Java\"\n")); + assertFalse(yaml.contains("data_model")); + assertTrue(yaml.contains(" value: \"\\\\result == 3\"\n format: \"java_expression\"\n")); + assertTrue(yaml.contains(" line: 4\n column: 32\n")); + assertTrue(yaml.contains(" value: \"\\\\result.equals(\\\"a\\\\\\\"b\\\")\"\n")); + assertTrue(yaml.contains(" line: 5\n column: 38\n")); + assertTrue(yaml.contains(" type: \"target\"\n action: \"follow\"\n location:\n")); + assertTrue(yaml.contains(" line: 7\n column: 7\n")); + assertEquals(3, yaml.split(" - segment:\n").length - 1); + + String graphml = Files.readString(out.resolve("witness.graphml")); + assertTrue(graphml.contains("x = 3")); + assertTrue(graphml.contains("s.equals(\"a\\\"b\")")); + assertTrue(graphml.contains("true")); + } + + @Test + void defaultViolationWithoutMarkers(@TempDir Path dir) throws IOException { + Path task = dir.resolve("task"); + Files.createDirectories(task); + Files.writeString(task.resolve("Main.java"), "public class Main {\n public static void main(String[] a) {\n assert false;\n }\n}\n"); + Path out = dir.resolve("out"); + + App.main(new String[] {"", "--input", task.toString(), "--out", out.toString()}); + String yaml = Files.readString(out.resolve("witness.yml")); + assertTrue(yaml.contains(" content: []\n")); + assertTrue(Files.readString(out.resolve("witness.graphml")).contains("true")); + + // A target alone is enough for a final segment + App.main(new String[] { + Base64.getEncoder().encodeToString( + marker("[WITNESS_TARGET]", "java.lang.AssertionError@@@Main@@Main.java@@3").getBytes()), + "--input", task.toString(), "--out", out.toString() + }); + yaml = Files.readString(out.resolve("witness.yml")); + assertTrue(yaml.contains(" type: \"target\"\n")); + assertTrue(yaml.contains(" line: 3\n column: 5\n")); + } + + @Test + void legacyInvocationStillWorks(@TempDir Path dir) throws IOException { + Files.writeString(dir.resolve("Main.java"), "class Main {\n int x = Verifier.nondetInt();\n}\n"); + String markers = marker("[WITNESS]", "1@@@2@@@Main@@@()V"); + App.main(new String[] {Base64.getEncoder().encodeToString(markers.getBytes()), dir.toString()}); + String yaml = Files.readString(dir.resolve("witness.yml")); + assertTrue(yaml.contains(" value: \"\\\\result == 1\"\n")); + assertTrue(yaml.contains(" line: 2\n column: 30\n")); + assertTrue(Files.readString(dir.resolve("witness.graphml")).contains("<init>")); + } + + private static String marker(String kind, String payload) { + return "prefix " + kind + " " + Base64.getEncoder().encodeToString(payload.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/witness/JavaLiteralsTest.java b/src/test/java/witness/JavaLiteralsTest.java new file mode 100644 index 0000000..72cfc62 --- /dev/null +++ b/src/test/java/witness/JavaLiteralsTest.java @@ -0,0 +1,77 @@ +package witness; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class JavaLiteralsTest { + + @Test + void integralTypes() { + assertEquals("\\result == 5", JavaLiterals.resultConstraint("5", "I")); + assertEquals("\\result == -5", JavaLiterals.resultConstraint("-5", "I")); + assertEquals("\\result == -128", JavaLiterals.resultConstraint("-128", "B")); + assertEquals("\\result == 32767", JavaLiterals.resultConstraint("32767", "S")); + assertEquals("\\result == -2147483648", JavaLiterals.resultConstraint("-2147483648", "I")); + } + + @Test + void longsGetSuffixOnlyWhenNeeded() { + assertEquals("\\result == 5", JavaLiterals.resultConstraint("5", "J")); + assertEquals("\\result == 5000000000L", JavaLiterals.resultConstraint("5000000000", "J")); + assertEquals( + "\\result == -9223372036854775808L", + JavaLiterals.resultConstraint("-9223372036854775808", "J")); + } + + @Test + void booleans() { + assertEquals("\\result == true", JavaLiterals.resultConstraint("true", "Z")); + assertEquals("\\result == false", JavaLiterals.resultConstraint("false", "Z")); + } + + @Test + void charsAreRenderedAsLiterals() { + assertEquals("\\result == 'a'", JavaLiterals.resultConstraint("97", "C")); + assertEquals("\\result == '\\''", JavaLiterals.resultConstraint("39", "C")); + assertEquals("\\result == '\\\\'", JavaLiterals.resultConstraint("92", "C")); + assertEquals("\\result == '\\n'", JavaLiterals.resultConstraint("10", "C")); + assertEquals("\\result == '\\u0000'", JavaLiterals.resultConstraint("0", "C")); + assertEquals("\\result == '\\u00e4'", JavaLiterals.resultConstraint("228", "C")); + assertEquals("\\result == '\\uffff'", JavaLiterals.resultConstraint("65535", "C")); + } + + @Test + void floatingPoint() { + assertEquals("\\result == 1.5f", JavaLiterals.resultConstraint("1.5", "F")); + assertEquals("\\result == -0.000002022784f", JavaLiterals.resultConstraint("-0.000002022784", "F")); + assertEquals("\\result == 10000000000.0f", JavaLiterals.resultConstraint("1.0E10", "F")); + assertEquals("\\result == 0.1", JavaLiterals.resultConstraint("0.1", "D")); + assertEquals("\\result == -0.0", JavaLiterals.resultConstraint("-0.0", "D")); + assertEquals("\\result == 10000000000.0", JavaLiterals.resultConstraint("10000000000", "D")); + assertEquals("Float.isNaN(\\result)", JavaLiterals.resultConstraint("NaN", "F")); + assertEquals("Double.isNaN(\\result)", JavaLiterals.resultConstraint("NaN", "D")); + assertEquals("\\result == Double.POSITIVE_INFINITY", JavaLiterals.resultConstraint("Infinity", "D")); + assertEquals("\\result == Float.NEGATIVE_INFINITY", JavaLiterals.resultConstraint("-Infinity", "F")); + } + + @Test + void strings() { + assertEquals("\\result.equals(\"abc\")", JavaLiterals.resultConstraint("abc", "Ljava/lang/String;")); + assertEquals("\\result.equals(\"\")", JavaLiterals.resultConstraint("", "Ljava/lang/String;")); + assertEquals( + "\\result.equals(\"a\\\"b\\\\c\\nd\\te\")", + JavaLiterals.resultConstraint("a\"b\\c\nd\te", "Ljava/lang/String;")); + assertEquals( + "\\result.equals(\"\\u0001x\")", + JavaLiterals.resultConstraint("\u0001x", "Ljava/lang/String;")); + assertEquals("\\result.equals(\"ä€\")", JavaLiterals.resultConstraint("ä€", "Ljava/lang/String;")); + } + + @Test + void legacyMarkersWithoutType() { + assertEquals("\\result == 42", JavaLiterals.resultConstraint("42", null)); + assertEquals("\\result == true", JavaLiterals.resultConstraint("true", null)); + assertEquals("\\result.equals(\"hi\")", JavaLiterals.resultConstraint("hi", null)); + } +} diff --git a/src/test/java/witness/MarkerTest.java b/src/test/java/witness/MarkerTest.java new file mode 100644 index 0000000..b9d4746 --- /dev/null +++ b/src/test/java/witness/MarkerTest.java @@ -0,0 +1,70 @@ +package witness; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MarkerTest { + + @Test + void parsesCurrentFormat() { + ValueMarker m = ValueMarker.parse("5@@@13@@@pkg/Main$1@@@main([Ljava/lang/String;)V@@@2@@@I@@@Main.java"); + assertEquals("5", m.getValue()); + assertEquals(13, m.getLine()); + assertEquals("pkg/Main$1", m.getClassName()); + assertEquals("main([Ljava/lang/String;)V", m.getMethodDesc()); + assertEquals(2, m.getCallIndex()); + assertEquals("I", m.getTypeDesc()); + assertEquals("Main.java", m.getSourceFile()); + } + + @Test + void stringValuesMayContainTheSeparator() { + ValueMarker m = ValueMarker.parse("a@@@b@@@7@@@Main@@@main()V@@@0@@@Ljava/lang/String;@@@Main.java"); + assertEquals("a@@@b", m.getValue()); + assertEquals(7, m.getLine()); + assertTrue(m.isString()); + + ValueMarker legacy = ValueMarker.parse("a@@@b@@@7@@@Main@@@main()V"); + assertEquals("a@@@b", legacy.getValue()); + assertEquals(7, legacy.getLine()); + } + + @Test + void parsesLegacyFormat() { + ValueMarker m = ValueMarker.parse("true@@@13@@@Main@@@main([Ljava/lang/String;)V"); + assertEquals("true", m.getValue()); + assertEquals(13, m.getLine()); + assertEquals("Main", m.getClassName()); + assertEquals(0, m.getCallIndex()); + assertNull(m.getTypeDesc()); + assertEquals("", m.getSourceFile()); + } + + @Test + void rejectsGarbage() { + assertThrows(IllegalArgumentException.class, () -> ValueMarker.parse("nonsense")); + } + + @Test + void parsesTarget() { + TargetMarker t = TargetMarker.parse( + "java.lang.AssertionError@@@Main@@Main.java@@23;pkg.Helper$Inner@@Helper.java@@5;java.lang.Thread@@@@-1"); + assertTrue(t.isAssertionError()); + assertEquals(3, t.getFrames().size()); + assertEquals("Main", t.getFrames().get(0).getClassName()); + assertEquals("Main.java", t.getFrames().get(0).getFileName()); + assertEquals(23, t.getFrames().get(0).getLine()); + assertEquals("", t.getFrames().get(2).getFileName()); + assertEquals(-1, t.getFrames().get(2).getLine()); + } + + @Test + void targetWithoutFrames() { + TargetMarker t = TargetMarker.parse("java.lang.RuntimeException@@@"); + assertEquals(0, t.getFrames().size()); + } +} From 33fd71066ee3a6b909cc5e7582f5cc0acfc6c83e Mon Sep 17 00:00:00 2001 From: Nils Loose Date: Thu, 15 Oct 2026 13:34:10 +0000 Subject: [PATCH 2/5] Replace the Java implementation by a Python package witnesscreator is a stdlib-only Python package (>= 3.10) with the library API agreed with SWAT (Task, Location, NondetReturn, Frame, Violation, write_witnesses), hand-written YAML 2.3 and GraphML emitters, and a CLI (`witnesscreator events|legacy`, plus the Java tool's positional command line for drop-in use). Ported from the Java branch with these fixes: - Java string literals escape every non-ASCII character (\uXXXX, surrogate pairs beyond the BMP); YAML scalars are plain ASCII with \uXXXX / \UXXXXXXXX escapes; all GraphML text is XML-escaped. - Target frames need a package match (path or `package` declaration); java./javax./jdk./sun./de.uzl.its.swat. frames are skipped; name-only fallback only for value locations. A missing target logs a warning. - Column heuristics run on source with comments and string/char literals blanked out; `nondet*(` needs a word boundary. - GraphML assumptions are `\result = ;` / `\result.equals("...");` (what wit4java parses) instead of a variable name guessed from the source line. - Never crashes on bad input; each format is written independently. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 18 +- MANIFEST.in | 3 + build.gradle | 71 ---- gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 7 - gradlew | 249 ----------- pyproject.toml | 51 +++ settings.gradle | 8 - src/main/java/App.java | 416 ------------------- src/main/java/witness/JavaLiterals.java | 160 ------- src/main/java/witness/SourceFiles.java | 203 --------- src/main/java/witness/TargetMarker.java | 90 ---- src/main/java/witness/ValueMarker.java | 119 ------ src/main/java/witness/WitnessAssumption.java | 31 -- src/main/java/witness/WitnessEdge.java | 31 -- src/main/java/witness/WitnessNode.java | 25 -- src/main/java/witness/YamlWitness.java | 176 -------- src/test/java/AppTest.java | 183 -------- src/test/java/witness/JavaLiteralsTest.java | 77 ---- src/test/java/witness/MarkerTest.java | 70 ---- src/witnesscreator/__init__.py | 36 ++ src/witnesscreator/__main__.py | 5 + src/witnesscreator/_version.py | 1 + src/witnesscreator/cli.py | 164 ++++++++ src/witnesscreator/columns.py | 126 ++++++ src/witnesscreator/events.py | 166 ++++++++ src/witnesscreator/graphml_writer.py | 122 ++++++ src/witnesscreator/literals.py | 209 ++++++++++ src/witnesscreator/markers.py | 152 +++++++ src/witnesscreator/model.py | 125 ++++++ src/witnesscreator/sources.py | 263 ++++++++++++ src/witnesscreator/witness.py | 283 +++++++++++++ src/witnesscreator/yaml_writer.py | 126 ++++++ tools/build_pyz.py | 35 ++ witnesses/default_violation.st | 56 --- witnesses/witness.st | 70 ---- 36 files changed, 1881 insertions(+), 2046 deletions(-) create mode 100644 MANIFEST.in delete mode 100644 build.gradle delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew create mode 100644 pyproject.toml delete mode 100644 settings.gradle delete mode 100755 src/main/java/App.java delete mode 100644 src/main/java/witness/JavaLiterals.java delete mode 100644 src/main/java/witness/SourceFiles.java delete mode 100644 src/main/java/witness/TargetMarker.java delete mode 100644 src/main/java/witness/ValueMarker.java delete mode 100644 src/main/java/witness/WitnessAssumption.java delete mode 100644 src/main/java/witness/WitnessEdge.java delete mode 100644 src/main/java/witness/WitnessNode.java delete mode 100644 src/main/java/witness/YamlWitness.java delete mode 100644 src/test/java/AppTest.java delete mode 100644 src/test/java/witness/JavaLiteralsTest.java delete mode 100644 src/test/java/witness/MarkerTest.java create mode 100644 src/witnesscreator/__init__.py create mode 100644 src/witnesscreator/__main__.py create mode 100644 src/witnesscreator/_version.py create mode 100644 src/witnesscreator/cli.py create mode 100644 src/witnesscreator/columns.py create mode 100644 src/witnesscreator/events.py create mode 100644 src/witnesscreator/graphml_writer.py create mode 100644 src/witnesscreator/literals.py create mode 100644 src/witnesscreator/markers.py create mode 100644 src/witnesscreator/model.py create mode 100644 src/witnesscreator/sources.py create mode 100644 src/witnesscreator/witness.py create mode 100644 src/witnesscreator/yaml_writer.py create mode 100644 tools/build_pyz.py delete mode 100755 witnesses/default_violation.st delete mode 100755 witnesses/witness.st diff --git a/.gitignore b/.gitignore index 8642f23..5772f43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,18 @@ -.gradle/ +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ build/ -out/ -*.class +dist/ +*.pyz +.pytest_cache/ +.venv/ +venv/ *.log .DS_Store -*.iml .idea/ +*.iml +# leftovers of the Java build +.gradle/ +out/ +*.class diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bc8afda --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include tests * +recursive-include tools *.py +global-exclude __pycache__ *.py[cod] diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 2b5f58d..0000000 --- a/build.gradle +++ /dev/null @@ -1,71 +0,0 @@ -plugins { - id 'application' - id 'com.github.johnrengelman.shadow' version '8.1.1' -} - -group = 'de.uzl.its.swat' -version = providers.gradleProperty('witnessCreatorVersion') - .orElse('0.1.0-SNAPSHOT') - .get() - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.antlr:ST4:4.3.4' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -tasks.named('test') { - useJUnitPlatform() -} - -application { - mainClass = 'App' -} - -jar { - manifest { - attributes('Main-Class': application.mainClass.get()) - } -} - -shadowJar { - archiveBaseName.set('WitnessCreator') - archiveClassifier.set('') - archiveVersion.set('') -} - -def shadowJarTask = tasks.named('shadowJar') - -tasks.register('svcompDistributionZip', Zip) { - dependsOn shadowJarTask - - archiveBaseName.set('witness-creator') - archiveVersion.set(project.version.toString()) - destinationDirectory.set(layout.buildDirectory.dir('distributions')) - - into('WitnessCreator/build/libs') { - from(shadowJarTask.flatMap { it.archiveFile }) - rename { 'WitnessCreator.jar' } - } - - into('WitnessCreator/witnesses') { - from('witnesses') { - include '*.st' - } - } -} - -build { - dependsOn shadowJar - dependsOn svcompDistributionZip -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3fa8f86..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 0adc8e1..0000000 --- a/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ef473de --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "witnesscreator" +dynamic = ["version"] +description = "SV-COMP violation witnesses for Java: YAML format 2.3 and legacy GraphML" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.10" +authors = [{ name = "Nils Loose" }] +keywords = ["sv-comp", "witness", "java", "verification"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Testing", +] +dependencies = [] + +[project.optional-dependencies] +test = ["pytest>=7", "PyYAML>=6"] + +[project.scripts] +witnesscreator = "witnesscreator.cli:main" + +[project.urls] +Homepage = "https://github.com/SWAT-project/WitnessCreator" +Issues = "https://github.com/SWAT-project/WitnessCreator/issues" + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.dynamic] +version = { attr = "witnesscreator._version.__version__" } + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra" diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index efe09d0..0000000 --- a/settings.gradle +++ /dev/null @@ -1,8 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -rootProject.name = 'WitnessCreator' diff --git a/src/main/java/App.java b/src/main/java/App.java deleted file mode 100755 index a9e153a..0000000 --- a/src/main/java/App.java +++ /dev/null @@ -1,416 +0,0 @@ -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.stringtemplate.v4.ST; -import org.stringtemplate.v4.STGroup; -import org.stringtemplate.v4.STRawGroupDir; - -import witness.JavaLiterals; -import witness.SourceFiles; -import witness.SourceFiles.InputFile; -import witness.TargetMarker; -import witness.ValueMarker; -import witness.WitnessAssumption; -import witness.WitnessEdge; -import witness.WitnessNode; -import witness.YamlWitness; -import witness.YamlWitness.Waypoint; - -/** - * Creates SV-COMP violation witnesses from SWAT's witness markers. - * - *

- * App <markers> [<dir>] [--input <path>]... [--property <file.prp>] [--out <dir>]
- *     [--producer-version <version>]
- * 
- * - *
    - *
  • {@code markers}: base64 of the output lines carrying {@code [WITNESS]} and - * {@code [WITNESS_TARGET]} markers (empty for the default violation witness). - *
  • {@code dir}: legacy positional argument, the directory holding the task sources; also - * the output directory unless {@code --out} is given. - *
  • {@code --input}: the task's input files or directories exactly as given to the verifier, - * used for the source lookup, the witness' {@code input_files} and their hashes. - *
  • {@code --property}: the property file, its content is the witness' specification. - *
- * - * Writes {@code witness.yml} (format 2.3) and, for the validators still consuming GraphML, - * {@code witness.graphml}. - */ -public class App { - - private static final String PRODUCER = "SWAT"; - private static final String DEFAULT_SPECIFICATION = "CHECK( init(Main.main()), LTL(G assert) )"; - private static final Pattern NONDET_CALL = Pattern.compile("nondet[A-Za-z0-9_]*\\s*\\("); - private static final Pattern ASSERT_KEYWORD = Pattern.compile("\\bassert\\b"); - private static final Pattern THROW_KEYWORD = Pattern.compile("\\bthrow\\b"); - - private final SourceFiles sourceFiles; - private final Path outDir; - - private App(SourceFiles sourceFiles, Path outDir) { - this.sourceFiles = sourceFiles; - this.outDir = outDir; - } - - public static void main(String[] args) throws IOException { - List positional = new ArrayList<>(); - List inputs = new ArrayList<>(); - String property = null; - String out = null; - String producerVersion = "unknown"; - for (int i = 0; i < args.length; i++) { - switch (args[i]) { - case "--input" -> inputs.add(args[++i]); - case "--property" -> property = args[++i]; - case "--out" -> out = args[++i]; - case "--producer-version" -> producerVersion = args[++i]; - default -> positional.add(args[i]); - } - } - if (positional.isEmpty()) { - System.err.println( - "Usage: App [] [--input ]... [--property ]" - + " [--out ] [--producer-version ]"); - System.exit(2); - } - String markers = positional.get(0); - String legacyDir = positional.size() > 1 ? positional.get(1) : null; - if (inputs.isEmpty() && legacyDir != null) { - inputs.add(legacyDir); - } - if (out == null) { - out = legacyDir != null ? legacyDir : "."; - } - Path outDir = Paths.get(out); - Files.createDirectories(outDir); - System.out.println("inputs: " + inputs); - System.out.println("output directory: " + outDir); - - SourceFiles sourceFiles = SourceFiles.collect(inputs); - String specification = readSpecification(property); - - List values = new ArrayList<>(); - TargetMarker target = null; - if (!markers.trim().isEmpty()) { - String decoded = new String(Base64.getDecoder().decode(markers.trim()), StandardCharsets.UTF_8); - for (String line : decoded.split("\n")) { - int idx = line.indexOf("[WITNESS_TARGET]"); - if (idx >= 0) { - target = TargetMarker.parse(decodePayload(line.substring(idx + "[WITNESS_TARGET]".length()))); - continue; - } - idx = line.indexOf("[WITNESS]"); - if (idx >= 0) { - values.add(ValueMarker.parse(decodePayload(line.substring(idx + "[WITNESS]".length())))); - } - } - } - if (values.isEmpty()) { - System.out.println("No witness values recorded, assuming default violation"); - } - if (target == null) { - System.out.println("No witness target recorded, the witness has no target waypoint"); - } - - App app = new App(sourceFiles, outDir); - app.saveYamlWitness(values, target, specification, producerVersion); - app.saveGraphmlWitness(values); - } - - private static String decodePayload(String base64) { - return new String(Base64.getDecoder().decode(base64.trim()), StandardCharsets.UTF_8); - } - - private static String readSpecification(String property) { - if (property == null) { - System.out.println("No property file given, assuming specification: " + DEFAULT_SPECIFICATION); - return DEFAULT_SPECIFICATION; - } - try { - String spec = Files.readString(Paths.get(property), StandardCharsets.UTF_8).trim(); - if (!spec.isEmpty()) { - return spec; - } - } catch (IOException e) { - System.out.println("Could not read property file " + property + ": " + e.getMessage()); - } - System.out.println("Assuming specification: " + DEFAULT_SPECIFICATION); - return DEFAULT_SPECIFICATION; - } - - // ------------------------------------------------------------------ - // YAML witness (format 2.3) - // ------------------------------------------------------------------ - - private void saveYamlWitness( - List values, TargetMarker target, String specification, String producerVersion) - throws IOException { - YamlWitness witness = new YamlWitness(PRODUCER, producerVersion, specification, sourceFiles); - - for (ValueMarker value : values) { - Optional file = sourceFiles.resolve(value.getClassName(), value.getSourceFile()); - String fileName; - Integer column = null; - if (file.isPresent()) { - fileName = file.get().getDisplayPath(); - String loc = file.get().getLine(value.getLine()); - int col = loc == null ? -1 : nondetCallColumn(loc, value.getCallIndex()); - if (col > 0) { - column = col; - } else { - System.out.println( - "Could not locate nondet call " + value.getCallIndex() + " in " + fileName - + ":" + value.getLine() + " (" + loc + ")"); - } - } else { - fileName = fallbackFileName(value.getClassName(), value.getSourceFile()); - System.out.println("Could not find the source of " + value.getClassName() + ", using " + fileName); - } - String constraint = JavaLiterals.resultConstraint(value.getValue(), value.getTypeDesc()); - witness.add(Waypoint.functionReturn(constraint, fileName, value.getLine(), column)); - } - - if (target != null) { - Optional targetWaypoint = targetWaypoint(target); - if (targetWaypoint.isPresent()) { - witness.add(targetWaypoint.get()); - } else { - System.out.println("No stack frame of " + target.getThrowableClass() - + " lies in the task sources: " + target.getFrames()); - } - } - - for (Waypoint wp : witness.getWaypoints()) { - System.out.println("Waypoint: " + wp); - } - Path file = outDir.resolve("witness.yml"); - System.out.println("Writing violation witness to file: " + file); - Files.writeString(file, witness.render(), StandardCharsets.UTF_8); - } - - /** The innermost stack frame inside the task sources becomes the target. */ - private Optional targetWaypoint(TargetMarker target) { - for (TargetMarker.Frame frame : target.getFrames()) { - if (frame.getLine() < 1) { - continue; - } - Optional file = sourceFiles.resolve(frame.getClassName(), frame.getFileName()); - if (file.isEmpty()) { - continue; - } - String loc = file.get().getLine(frame.getLine()); - if (loc == null) { - System.out.println("Frame " + frame + " is outside of " + file.get().getPath()); - continue; - } - int col = statementColumn(loc, target.isAssertionError()); - return Optional.of( - Waypoint.target(file.get().getDisplayPath(), frame.getLine(), col > 0 ? col : null)); - } - return Optional.empty(); - } - - /** - * The 1-based column of the closing parenthesis of the {@code callIndex}-th nondet call in the - * line, -1 if there is none. - */ - static int nondetCallColumn(String line, int callIndex) { - Matcher m = NONDET_CALL.matcher(line); - int found = 0; - while (m.find()) { - if (found++ < callIndex) { - continue; - } - int open = m.end() - 1; - int depth = 0; - for (int i = open; i < line.length(); i++) { - char c = line.charAt(i); - if (c == '(') { - depth++; - } else if (c == ')') { - depth--; - if (depth == 0) { - return i + 1; - } - } - } - return -1; - } - return -1; - } - - /** - * The 1-based column of the first character of the violating statement on the line: the - * {@code assert} of a failed assertion, the {@code throw} of an explicit throw, otherwise the - * first token that is not a closing brace or an {@code else}. - */ - static int statementColumn(String line, boolean assertion) { - if (assertion) { - Matcher m = ASSERT_KEYWORD.matcher(line); - if (m.find()) { - return m.start() + 1; - } - } - Matcher t = THROW_KEYWORD.matcher(line); - if (t.find()) { - return t.start() + 1; - } - int i = skipWhitespace(line, 0); - while (i < line.length()) { - if (line.charAt(i) == '}' || line.charAt(i) == '{') { - i = skipWhitespace(line, i + 1); - } else if (line.startsWith("else", i) - && (i + 4 == line.length() || !Character.isJavaIdentifierPart(line.charAt(i + 4)))) { - i = skipWhitespace(line, i + 4); - } else { - break; - } - } - return i < line.length() ? i + 1 : -1; - } - - private static int skipWhitespace(String line, int i) { - while (i < line.length() && Character.isWhitespace(line.charAt(i))) { - i++; - } - return i; - } - - private static String fallbackFileName(String className, String sourceFile) { - String internal = className.replace('.', '/'); - int slash = internal.lastIndexOf('/'); - String pkgDir = slash < 0 ? "" : internal.substring(0, slash + 1); - if (sourceFile != null && !sourceFile.isEmpty()) { - return pkgDir + sourceFile; - } - String simple = internal.substring(slash + 1); - int dollar = simple.indexOf('$'); - return pkgDir + (dollar < 0 ? simple : simple.substring(0, dollar)) + ".java"; - } - - // ------------------------------------------------------------------ - // GraphML witness (legacy format, kept for the GraphML-based validators) - // ------------------------------------------------------------------ - - private void saveGraphmlWitness(List values) { - STGroup group = new STRawGroupDir(templateDir(), '$', '$'); - ST st; - if (values.isEmpty()) { - st = group.getInstanceOf("default_violation"); - } else { - List nodes = new ArrayList<>(); - List edges = new ArrayList<>(); - - int nodeId = 0; - WitnessNode initNode = new WitnessNode(nodeId++); - nodes.add(initNode); - initNode.addData("entry", "true"); - WitnessNode curNode = initNode; - - for (ValueMarker value : values) { - Optional file = sourceFiles.resolve(value.getClassName(), value.getSourceFile()); - String clazz = file.map(InputFile::getRelativePath) - .orElse(fallbackFileName(value.getClassName(), value.getSourceFile())); - String loc = file.map(f -> f.getLine(value.getLine())).orElse(null); - // Escape XML special characters in scope to avoid XML parse errors - // (e.g., contains angle brackets that break XML parsing) - String scope = "L" + value.getClassName() + ";." + value.getMethodDesc(); - scope = scope.replace("&", "&").replace("<", "<").replace(">", ">"); - WitnessAssumption wa = new WitnessAssumption(value.getValue(), clazz, scope, value.getLine()); - String assumption = computeAssumption(loc, wa, value.isString()); - - WitnessNode prevNode = curNode; - curNode = new WitnessNode(nodeId++); - nodes.add(curNode); - edges.add(new WitnessEdge(prevNode, wa, assumption, curNode)); - } - curNode.addData("violation", "true"); - - st = group.getInstanceOf("witness"); - st.add("nodes", nodes); - st.add("edges", edges); - } - Path file = outDir.resolve("witness.graphml"); - try { - System.out.println("Writing violation witness to file: " + file); - Files.writeString(file, st.render(), StandardCharsets.UTF_8); - } catch (IOException e) { - System.out.println("Error writing witness to file: " + e.getMessage()); - } - } - - /** The {@code witnesses} template directory next to the jar, or relative to the cwd. */ - private static String templateDir() { - if (Files.isDirectory(Paths.get("witnesses"))) { - return "witnesses"; - } - try { - Path jar = Paths.get(App.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - for (Path dir = jar.getParent(); dir != null; dir = dir.getParent()) { - Path candidate = dir.resolve("witnesses"); - if (Files.isDirectory(candidate)) { - return candidate.toString(); - } - } - } catch (URISyntaxException | RuntimeException e) { - // fall through - } - return "witnesses"; - } - - private String computeAssumption(String lineOfCode, WitnessAssumption wa, boolean knownString) { - if (lineOfCode == null) { - return "true"; - } - - // Check if this is a string type by looking for nondetString() in the line - boolean isStringType = knownString || lineOfCode.contains("nondetString()"); - - int idx = lineOfCode.indexOf("Verifier"); - if (idx < 0) { - return "true"; - } - lineOfCode = lineOfCode.substring(0, idx).trim(); - idx = lineOfCode.lastIndexOf("="); - if (idx < 0) { - // No assignment found - this is likely a return statement or direct call - // Use the witness value directly (e.g., "true", "false", or a literal value) - return wa.getValue(); - } - // Check if this is a comparison (==) not an assignment (=) - // If the character before '=' is also '=', this is a comparison, not an assignment - if (idx > 0 && lineOfCode.charAt(idx - 1) == '=') { - // This is a comparison like "if (pos == ..." - no variable assignment - return wa.getValue(); - } - lineOfCode = lineOfCode.substring(0, idx).trim(); - String[] parts = lineOfCode.split(" "); - String id = parts[parts.length - 1].trim(); - - // For string types, always use .equals() format with properly quoted value - if (isStringType) { - String value = wa.getValue(); - // Add quotes around the value if not already present - if (!value.startsWith("\"")) { - // Escape any quotes and backslashes within the string value - value = value.replace("\\", "\\\\").replace("\"", "\\\""); - value = "\"" + value + "\""; - } - return id + ".equals(" + value + ")"; - } - - // For primitive types, use assignment format - return id + " = " + wa.getValue(); - } -} diff --git a/src/main/java/witness/JavaLiterals.java b/src/main/java/witness/JavaLiterals.java deleted file mode 100644 index 9a2a558..0000000 --- a/src/main/java/witness/JavaLiterals.java +++ /dev/null @@ -1,160 +0,0 @@ -package witness; - -import java.math.BigDecimal; - -/** Renders recorded values as {@code java_expression} constraints over {@code \result}. */ -public final class JavaLiterals { - private static final String RESULT = "\\result"; - - private JavaLiterals() {} - - /** - * The constraint satisfied exactly when the function returned {@code value}. - * - * @param value the value as recorded by the executor - * @param typeDesc the JVM descriptor of the returned type, {@code null} if unknown (legacy) - */ - public static String resultConstraint(String value, String typeDesc) { - if (typeDesc == null) { - return legacyConstraint(value); - } - switch (typeDesc) { - case "Z": - return RESULT + " == " + ("true".equals(value) ? "true" : "false"); - case "B": - case "S": - case "I": - return RESULT + " == " + value; - case "J": - return RESULT + " == " + longLiteral(value); - case "C": - return RESULT + " == " + charLiteral(value); - case "F": - return floatingConstraint(value, true); - case "D": - return floatingConstraint(value, false); - case "Ljava/lang/String;": - return RESULT + ".equals(" + stringLiteral(value) + ")"; - default: - return legacyConstraint(value); - } - } - - private static String legacyConstraint(String value) { - if (value.matches("-?\\d+") || "true".equals(value) || "false".equals(value)) { - return RESULT + " == " + value; - } - return RESULT + ".equals(" + stringLiteral(value) + ")"; - } - - private static String longLiteral(String value) { - try { - long l = Long.parseLong(value); - if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) { - return value; - } - return value + "L"; - } catch (NumberFormatException e) { - return value; - } - } - - private static String floatingConstraint(String value, boolean isFloat) { - String box = isFloat ? "Float" : "Double"; - switch (value) { - case "NaN": - return box + ".isNaN(" + RESULT + ")"; - case "Infinity": - return RESULT + " == " + box + ".POSITIVE_INFINITY"; - case "-Infinity": - return RESULT + " == " + box + ".NEGATIVE_INFINITY"; - default: - break; - } - String literal = value; - try { - // Normalises scientific notation and guarantees a decimal point; BigDecimal has no - // negative zero, so the sign is kept separately - boolean negative = value.startsWith("-"); - literal = new BigDecimal(negative ? value.substring(1) : value).toPlainString(); - if (negative) { - literal = "-" + literal; - } - } catch (NumberFormatException e) { - // keep as is - } - if (!literal.contains(".")) { - literal += ".0"; - } - return RESULT + " == " + literal + (isFloat ? "f" : ""); - } - - /** Char values are recorded as their code point. */ - public static String charLiteral(String codePoint) { - int c; - try { - c = Integer.parseInt(codePoint); - } catch (NumberFormatException e) { - return codePoint; - } - switch (c) { - case '\'': - return "'\\''"; - case '\\': - return "'\\\\'"; - case '\n': - return "'\\n'"; - case '\r': - return "'\\r'"; - case '\t': - return "'\\t'"; - case '\b': - return "'\\b'"; - case '\f': - return "'\\f'"; - default: - break; - } - if (c >= 0x20 && c < 0x7f) { - return "'" + (char) c + "'"; - } - return String.format("'\\u%04x'", c); - } - - public static String stringLiteral(String value) { - StringBuilder sb = new StringBuilder("\""); - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - switch (c) { - case '"': - sb.append("\\\""); - break; - case '\\': - sb.append("\\\\"); - break; - case '\n': - sb.append("\\n"); - break; - case '\r': - sb.append("\\r"); - break; - case '\t': - sb.append("\\t"); - break; - case '\b': - sb.append("\\b"); - break; - case '\f': - sb.append("\\f"); - break; - default: - if (c < 0x20 || c == 0x7f) { - sb.append(String.format("\\u%04x", (int) c)); - } else { - sb.append(c); - } - } - } - return sb.append('"').toString(); - } -} diff --git a/src/main/java/witness/SourceFiles.java b/src/main/java/witness/SourceFiles.java deleted file mode 100644 index bee2b45..0000000 --- a/src/main/java/witness/SourceFiles.java +++ /dev/null @@ -1,203 +0,0 @@ -package witness; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; - -/** - * The Java source files of the verification task, as they were given to the verifier (files or - * directories). Resolves classes to their source files and provides line contents and hashes. - */ -public final class SourceFiles { - - /** One input file of the task. */ - public static final class InputFile { - private final String displayPath; - private final Path path; - private final String relativePath; - private List lines; - private String sha256; - - InputFile(String displayPath, Path path, String relativePath) { - this.displayPath = displayPath; - this.path = path; - this.relativePath = relativePath; - } - - /** The path as it is written into the witness (input as given + relative path). */ - public String getDisplayPath() { - return displayPath; - } - - public Path getPath() { - return path; - } - - /** Path relative to the input root it was found in, '/'-separated. */ - public String getRelativePath() { - return relativePath; - } - - public String getFileName() { - return path.getFileName().toString(); - } - - public List getLines() { - if (lines == null) { - try { - lines = Files.readAllLines(path, StandardCharsets.UTF_8); - } catch (IOException e) { - System.out.println("Could not read " + path + ": " + e.getMessage()); - lines = Collections.emptyList(); - } - } - return lines; - } - - /** The 1-based line, or {@code null} if the file does not have it. */ - public String getLine(int lineNumber) { - List l = getLines(); - if (lineNumber < 1 || lineNumber > l.size()) { - return null; - } - return l.get(lineNumber - 1); - } - - public String getSha256() throws IOException { - if (sha256 == null) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(Files.readAllBytes(path)); - StringBuilder sb = new StringBuilder(); - for (byte b : hash) { - sb.append(String.format("%02x", b)); - } - sha256 = sb.toString(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - return sha256; - } - } - - private final List files; - - private SourceFiles(List files) { - this.files = Collections.unmodifiableList(files); - } - - /** - * Collects all {@code .java} files of the given inputs. Directories are searched recursively, - * the files are ordered by path inside each input. - */ - public static SourceFiles collect(List inputs) { - List files = new ArrayList<>(); - Set seen = new HashSet<>(); // a file reachable through several inputs is listed once - for (String input : inputs) { - Path root = Paths.get(input); - if (Files.isDirectory(root)) { - String prefix = stripTrailingSlash(input); - try (Stream walk = Files.walk(root)) { - List javaFiles = - walk.filter(Files::isRegularFile) - .filter(p -> p.getFileName().toString().endsWith(".java")) - .sorted() - .toList(); - for (Path p : javaFiles) { - if (seen.add(p.toAbsolutePath().normalize())) { - String rel = toSlashes(root.relativize(p).toString()); - files.add(new InputFile(prefix + "/" + rel, p, rel)); - } - } - } catch (IOException e) { - System.out.println("Could not read input directory " + input + ": " + e.getMessage()); - } - } else if (Files.isRegularFile(root)) { - if (root.getFileName().toString().endsWith(".java") - && seen.add(root.toAbsolutePath().normalize())) { - files.add(new InputFile(toSlashes(input), root, root.getFileName().toString())); - } - } else { - System.out.println("Ignoring missing input: " + input); - } - } - return new SourceFiles(files); - } - - public List getFiles() { - return files; - } - - public boolean isEmpty() { - return files.isEmpty(); - } - - /** - * Finds the source file of a class. - * - * @param className JVM internal ({@code pkg/Outer$Inner}) or dotted ({@code pkg.Outer$Inner}) - * class name - * @param sourceFileName the class' SourceFile attribute, may be empty or {@code null} - */ - public Optional resolve(String className, String sourceFileName) { - String internal = className.replace('.', '/'); - int slash = internal.lastIndexOf('/'); - String pkgDir = slash < 0 ? "" : internal.substring(0, slash); - String simple = slash < 0 ? internal : internal.substring(slash + 1); - int dollar = simple.indexOf('$'); - String outer = dollar < 0 ? simple : simple.substring(0, dollar); - - List fileNames = new ArrayList<>(); - if (sourceFileName != null && !sourceFileName.isEmpty()) { - fileNames.add(sourceFileName); - } - fileNames.add(outer + ".java"); - - // Exact package path first, then any path ending with the package path, then by name only - for (String fileName : fileNames) { - String rel = pkgDir.isEmpty() ? fileName : pkgDir + "/" + fileName; - Optional match = find(f -> f.getRelativePath().equals(rel)); - if (match.isEmpty()) { - match = find(f -> f.getRelativePath().endsWith("/" + rel)); - } - if (match.isPresent()) { - return match; - } - } - for (String fileName : fileNames) { - Optional match = find(f -> f.getFileName().equals(fileName)); - if (match.isPresent()) { - return match; - } - } - return Optional.empty(); - } - - private Optional find(java.util.function.Predicate predicate) { - return files.stream().filter(predicate).findFirst(); - } - - private static String stripTrailingSlash(String s) { - String r = toSlashes(s); - while (r.length() > 1 && r.endsWith("/")) { - r = r.substring(0, r.length() - 1); - } - return r; - } - - private static String toSlashes(String s) { - return s.replace('\\', '/'); - } -} diff --git a/src/main/java/witness/TargetMarker.java b/src/main/java/witness/TargetMarker.java deleted file mode 100644 index a74f253..0000000 --- a/src/main/java/witness/TargetMarker.java +++ /dev/null @@ -1,90 +0,0 @@ -package witness; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * A decoded {@code [WITNESS_TARGET]} marker: the uncaught throwable that ended the violating run - * together with its stack trace (innermost frame first). - * - *

Payload format: {@code throwableClass@@@className@@fileName@@line;className@@fileName@@line;...} - */ -public final class TargetMarker { - private final String throwableClass; - private final List frames; - - public static final class Frame { - private final String className; - private final String fileName; - private final int line; - - Frame(String className, String fileName, int line) { - this.className = className; - this.fileName = fileName; - this.line = line; - } - - /** Fully qualified (dotted) class name. */ - public String getClassName() { - return className; - } - - /** Source file name from the stack trace, empty if unknown. */ - public String getFileName() { - return fileName; - } - - public int getLine() { - return line; - } - - @Override - public String toString() { - return className + "(" + fileName + ":" + line + ")"; - } - } - - private TargetMarker(String throwableClass, List frames) { - this.throwableClass = throwableClass; - this.frames = Collections.unmodifiableList(frames); - } - - public static TargetMarker parse(String payload) { - int idx = payload.indexOf("@@@"); - if (idx < 0) { - throw new IllegalArgumentException("Invalid witness target marker: " + payload); - } - String throwableClass = payload.substring(0, idx).trim(); - List frames = new ArrayList<>(); - String rest = payload.substring(idx + 3).trim(); - if (!rest.isEmpty()) { - for (String frame : rest.split(";")) { - String[] parts = frame.split("@@", -1); - if (parts.length != 3) { - continue; - } - int line; - try { - line = Integer.parseInt(parts[2].trim()); - } catch (NumberFormatException e) { - line = -1; - } - frames.add(new Frame(parts[0].trim(), parts[1].trim(), line)); - } - } - return new TargetMarker(throwableClass, frames); - } - - public String getThrowableClass() { - return throwableClass; - } - - public boolean isAssertionError() { - return "java.lang.AssertionError".equals(throwableClass); - } - - public List getFrames() { - return frames; - } -} diff --git a/src/main/java/witness/ValueMarker.java b/src/main/java/witness/ValueMarker.java deleted file mode 100644 index 1abf53d..0000000 --- a/src/main/java/witness/ValueMarker.java +++ /dev/null @@ -1,119 +0,0 @@ -package witness; - -import java.util.Arrays; -import java.util.Set; - -/** - * A decoded {@code [WITNESS]} marker: one value returned by a {@code Verifier.nondet*()} call. - * - *

Payload format (fields separated by {@code @@@}): - * {@code value@@@line@@@className@@@methodDesc@@@callIndex@@@typeDesc@@@sourceFile}. - * The legacy format {@code value@@@line@@@className@@@methodDesc} is still accepted. Because a - * String value may itself contain the separator, the fields are parsed from the end. - */ -public final class ValueMarker { - private static final String SEPARATOR = "@@@"; - private static final Set TYPE_DESCRIPTORS = - Set.of("Z", "B", "C", "S", "I", "J", "F", "D", "Ljava/lang/String;"); - - private final String value; - private final int line; - private final String className; - private final String methodDesc; - private final int callIndex; - private final String typeDesc; - private final String sourceFile; - - private ValueMarker( - String value, - int line, - String className, - String methodDesc, - int callIndex, - String typeDesc, - String sourceFile) { - this.value = value; - this.line = line; - this.className = className; - this.methodDesc = methodDesc; - this.callIndex = callIndex; - this.typeDesc = typeDesc; - this.sourceFile = sourceFile; - } - - public static ValueMarker parse(String payload) { - String[] parts = payload.split(SEPARATOR, -1); - int n = parts.length; - if (n >= 7 && TYPE_DESCRIPTORS.contains(parts[n - 2].trim()) && isInt(parts[n - 3])) { - return new ValueMarker( - join(parts, n - 6), - Integer.parseInt(parts[n - 6].trim()), - parts[n - 5].trim(), - parts[n - 4].trim(), - Integer.parseInt(parts[n - 3].trim()), - parts[n - 2].trim(), - parts[n - 1].trim()); - } - if (n >= 4 && isInt(parts[n - 3])) { - return new ValueMarker( - join(parts, n - 3), - Integer.parseInt(parts[n - 3].trim()), - parts[n - 2].trim(), - parts[n - 1].trim(), - 0, - null, - ""); - } - throw new IllegalArgumentException("Invalid witness marker: " + payload); - } - - private static String join(String[] parts, int count) { - return String.join(SEPARATOR, Arrays.copyOfRange(parts, 0, count)); - } - - private static boolean isInt(String s) { - try { - Integer.parseInt(s.trim()); - return true; - } catch (NumberFormatException e) { - return false; - } - } - - /** The recorded value as printed by the executor (chars as their code point). */ - public String getValue() { - return value; - } - - public int getLine() { - return line; - } - - /** JVM internal class name of the caller, e.g. {@code pkg/Main$Inner}. */ - public String getClassName() { - return className; - } - - public String getMethodDesc() { - return methodDesc; - } - - /** 0-based index of the call among the nondet calls on the same source line. */ - public int getCallIndex() { - return callIndex; - } - - /** JVM descriptor of the returned type, or {@code null} for legacy markers. */ - public String getTypeDesc() { - return typeDesc; - } - - /** The caller's SourceFile attribute, empty if unknown. */ - public String getSourceFile() { - return sourceFile; - } - - public boolean isString() { - return "Ljava/lang/String;".equals(typeDesc); - } -} diff --git a/src/main/java/witness/WitnessAssumption.java b/src/main/java/witness/WitnessAssumption.java deleted file mode 100644 index 83fea6c..0000000 --- a/src/main/java/witness/WitnessAssumption.java +++ /dev/null @@ -1,31 +0,0 @@ -package witness; - -public class WitnessAssumption { - private final String value; - private final String clazz; - private final String scope; - private final int line; - - public WitnessAssumption(String value, String clazz, String scope, int line) { - this.value = value; - this.clazz = clazz; - this.scope = scope; - this.line = line; - } - - public String getScope() { - return scope; - } - - public String getValue() { - return value; - } - - public String getClazz() { - return clazz; - } - - public int getLine() { - return line; - } -} diff --git a/src/main/java/witness/WitnessEdge.java b/src/main/java/witness/WitnessEdge.java deleted file mode 100644 index 18a4fb6..0000000 --- a/src/main/java/witness/WitnessEdge.java +++ /dev/null @@ -1,31 +0,0 @@ -package witness; - -public class WitnessEdge { - private final WitnessNode source; - private final WitnessAssumption witness; - private final String assumption; - private final WitnessNode dest; - - public WitnessEdge(WitnessNode source, WitnessAssumption witness, String assumption, WitnessNode dest) { - this.source = source; - this.witness = witness; - this.assumption = assumption; - this.dest = dest; - } - - public WitnessNode getSource() { - return source; - } - - public WitnessNode getDest() { - return dest; - } - - public WitnessAssumption getWitness() { - return witness; - } - - public String getAssumption() { - return assumption; - } -} diff --git a/src/main/java/witness/WitnessNode.java b/src/main/java/witness/WitnessNode.java deleted file mode 100644 index 5e74e65..0000000 --- a/src/main/java/witness/WitnessNode.java +++ /dev/null @@ -1,25 +0,0 @@ -package witness; - -import java.util.LinkedHashMap; -import java.util.Map; - -public class WitnessNode { - private final int id; - private final Map data = new LinkedHashMap<>(); - - public WitnessNode(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public Map getData() { - return data; - } - - public void addData(String key, String value) { - data.put(key, value); - } -} diff --git a/src/main/java/witness/YamlWitness.java b/src/main/java/witness/YamlWitness.java deleted file mode 100644 index 7298c0f..0000000 --- a/src/main/java/witness/YamlWitness.java +++ /dev/null @@ -1,176 +0,0 @@ -package witness; - -import java.io.IOException; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * A violation witness in the YAML format, version 2.3, for Java (see - * https://gitlab.com/sosy-lab/benchmarking/sv-witnesses). The witness is a single - * {@code violation_sequence} whose segments each hold one {@code follow} waypoint: one - * {@code function_return} waypoint per recorded {@code Verifier.nondet*()} value and, if the - * violating statement is known, a final {@code target} waypoint. - */ -public final class YamlWitness { - public static final String FORMAT_VERSION = "2.3"; - - /** A follow waypoint. */ - public static final class Waypoint { - private final String type; - private final String constraint; - private final String fileName; - private final int line; - private final Integer column; - - private Waypoint(String type, String constraint, String fileName, int line, Integer column) { - this.type = type; - this.constraint = constraint; - this.fileName = fileName; - this.line = line; - this.column = column; - } - - public static Waypoint functionReturn( - String constraint, String fileName, int line, Integer column) { - return new Waypoint("function_return", constraint, fileName, line, column); - } - - public static Waypoint target(String fileName, int line, Integer column) { - return new Waypoint("target", null, fileName, line, column); - } - - public String getType() { - return type; - } - - @Override - public String toString() { - return type - + " @ " - + fileName - + ":" - + line - + (column == null ? "" : ":" + column) - + (constraint == null ? "" : " [" + constraint + "]"); - } - } - - private final String producerName; - private final String producerVersion; - private final String specification; - private final SourceFiles sourceFiles; - private final List waypoints = new ArrayList<>(); - - public YamlWitness( - String producerName, - String producerVersion, - String specification, - SourceFiles sourceFiles) { - this.producerName = producerName; - this.producerVersion = producerVersion; - this.specification = specification; - this.sourceFiles = sourceFiles; - } - - public void add(Waypoint waypoint) { - waypoints.add(waypoint); - } - - public List getWaypoints() { - return waypoints; - } - - public String render() throws IOException { - StringBuilder sb = new StringBuilder(); - sb.append("- entry_type: \"violation_sequence\"\n"); - sb.append(" metadata:\n"); - sb.append(" format_version: ").append(quote(FORMAT_VERSION)).append('\n'); - sb.append(" uuid: ").append(quote(UUID.randomUUID().toString())).append('\n'); - String time = - ZonedDateTime.now(ZoneOffset.UTC) - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")); - sb.append(" creation_time: ").append(quote(time)).append('\n'); - sb.append(" producer:\n"); - sb.append(" name: ").append(quote(producerName)).append('\n'); - sb.append(" version: ").append(quote(producerVersion)).append('\n'); - sb.append(" task:\n"); - if (sourceFiles.isEmpty()) { - sb.append(" input_files: []\n"); - sb.append(" input_file_hashes: {}\n"); - } else { - sb.append(" input_files:\n"); - for (SourceFiles.InputFile f : sourceFiles.getFiles()) { - sb.append(" - ").append(quote(f.getDisplayPath())).append('\n'); - } - sb.append(" input_file_hashes:\n"); - for (SourceFiles.InputFile f : sourceFiles.getFiles()) { - sb.append(" ") - .append(quote(f.getDisplayPath())) - .append(": ") - .append(quote(f.getSha256())) - .append('\n'); - } - } - sb.append(" specification: ").append(quote(specification)).append('\n'); - sb.append(" language: \"Java\"\n"); - if (waypoints.isEmpty()) { - sb.append(" content: []\n"); - return sb.toString(); - } - sb.append(" content:\n"); - for (Waypoint wp : waypoints) { - sb.append(" - segment:\n"); - sb.append(" - waypoint:\n"); - sb.append(" type: ").append(quote(wp.type)).append('\n'); - sb.append(" action: \"follow\"\n"); - if (wp.constraint != null) { - sb.append(" constraint:\n"); - sb.append(" value: ").append(quote(wp.constraint)).append('\n'); - sb.append(" format: \"java_expression\"\n"); - } - sb.append(" location:\n"); - sb.append(" file_name: ").append(quote(wp.fileName)).append('\n'); - sb.append(" line: ").append(wp.line).append('\n'); - if (wp.column != null) { - sb.append(" column: ").append(wp.column).append('\n'); - } - } - return sb.toString(); - } - - /** Renders a YAML double-quoted scalar. */ - static String quote(String s) { - StringBuilder sb = new StringBuilder("\""); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '"': - sb.append("\\\""); - break; - case '\\': - sb.append("\\\\"); - break; - case '\n': - sb.append("\\n"); - break; - case '\r': - sb.append("\\r"); - break; - case '\t': - sb.append("\\t"); - break; - default: - if (c < 0x20 || c == 0x7f) { - sb.append(String.format("\\u%04x", (int) c)); - } else { - sb.append(c); - } - } - } - return sb.append('"').toString(); - } -} diff --git a/src/test/java/AppTest.java b/src/test/java/AppTest.java deleted file mode 100644 index 3079d3e..0000000 --- a/src/test/java/AppTest.java +++ /dev/null @@ -1,183 +0,0 @@ -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import witness.SourceFiles; - -class AppTest { - - @Test - void columnOfClosingParenthesis() { - String line = " Boolean input_1_0 = Verifier.nondetBoolean();"; - assertEquals(48, App.nondetCallColumn(line, 0)); - assertEquals(-1, App.nondetCallColumn(line, 1)); - - String two = " if (!chunked && Verifier.nondetInt() > 0 && Verifier.nondetInt() != 0) {"; - assertEquals(')', two.charAt(App.nondetCallColumn(two, 0) - 1)); - assertEquals(')', two.charAt(App.nondetCallColumn(two, 1) - 1)); - assertTrue(App.nondetCallColumn(two, 0) < App.nondetCallColumn(two, 1)); - - String spaced = "x = Verifier.nondetInt ( );"; - assertEquals(26, App.nondetCallColumn(spaced, 0)); - assertEquals(-1, App.nondetCallColumn("no call here", 0)); - assertEquals(-1, App.nondetCallColumn("Verifier.nondetInt(", 0)); - } - - @Test - void columnOfViolatingStatement() { - assertEquals(12, App.statementColumn(" } else assert false;", true)); - assertEquals(5, App.statementColumn(" assert !(output_1 == 1);", true)); - assertEquals(5, App.statementColumn(" assert !(output_1 == 1);", false)); - assertEquals(7, App.statementColumn(" } throw new RuntimeException();", false)); - assertEquals(5, App.statementColumn(" int x = a / b;", false)); - assertEquals(14, App.statementColumn(" } else { int x = a / b; }", false)); - // tabs count as one character, like any other - assertEquals(4, App.statementColumn("\t\t\tx = elsewhere / 0;", false)); - assertEquals(-1, App.statementColumn(" }", false)); - assertEquals(-1, App.statementColumn("", false)); - } - - @Test - void resolvesSources(@TempDir Path dir) throws IOException { - Path task = dir.resolve("task"); - Files.createDirectories(task.resolve("pkg")); - Files.writeString(task.resolve("Main.java"), "class Main {}\nclass Helper {}\n"); - Files.writeString(task.resolve("pkg/Foo.java"), "package pkg;\nclass Foo {}\n"); - Path common = dir.resolve("common"); - Files.createDirectories(common.resolve("org/sosy_lab/sv_benchmarks")); - Files.writeString(common.resolve("org/sosy_lab/sv_benchmarks/Verifier.java"), "package org.sosy_lab.sv_benchmarks;\n"); - - SourceFiles files = SourceFiles.collect(List.of(common + "/", task.toString())); - assertEquals( - List.of( - common + "/org/sosy_lab/sv_benchmarks/Verifier.java", - task + "/Main.java", - task + "/pkg/Foo.java"), - files.getFiles().stream().map(SourceFiles.InputFile::getDisplayPath).toList()); - - assertEquals("Main.java", files.resolve("Main", "Main.java").get().getRelativePath()); - assertEquals("Main.java", files.resolve("Main$1", "").get().getRelativePath()); - assertEquals("Main.java", files.resolve("Helper", "Main.java").get().getRelativePath()); - assertEquals("pkg/Foo.java", files.resolve("pkg/Foo", "Foo.java").get().getRelativePath()); - assertEquals("pkg/Foo.java", files.resolve("pkg.Foo$Inner", "").get().getRelativePath()); - assertEquals( - "org/sosy_lab/sv_benchmarks/Verifier.java", - files.resolve("org.sosy_lab.sv_benchmarks.Verifier", "Verifier.java").get().getRelativePath()); - assertEquals(Optional.empty(), files.resolve("Missing", "")); - assertEquals(64, files.getFiles().get(0).getSha256().length()); - - // A file reachable through several inputs (e.g. a nested common/ dir) is listed once - SourceFiles nested = SourceFiles.collect(List.of(task + "/pkg", task.toString(), task + "/pkg/Foo.java")); - assertEquals( - List.of(task + "/pkg/Foo.java", task + "/Main.java"), - nested.getFiles().stream().map(SourceFiles.InputFile::getDisplayPath).toList()); - } - - @Test - void writesYamlAndGraphmlWitness(@TempDir Path dir) throws IOException { - Path task = dir.resolve("task"); - Files.createDirectories(task); - Files.writeString( - task.resolve("Main.java"), - String.join( - "\n", - "import org.sosy_lab.sv_benchmarks.Verifier;", - "public class Main {", - " public static void main(String[] args) {", - " int x = Verifier.nondetInt();", - " String s = Verifier.nondetString();", - " if (x == 3 && s.equals(\"a\\\"b\")) {", - " assert false;", - " }", - " }", - "}", - "")); - Path prp = dir.resolve("valid-assert.prp"); - Files.writeString(prp, "CHECK( init(Main.main()), LTL(G assert) )\n"); - Path out = dir.resolve("out"); - - String markers = - marker("[WITNESS]", "3@@@4@@@Main@@@main([Ljava/lang/String;)V@@@0@@@I@@@Main.java") - + "\n" - + marker("[WITNESS]", "a\"b@@@5@@@Main@@@main([Ljava/lang/String;)V@@@0@@@Ljava/lang/String;@@@Main.java") - + "\n" - + marker("[WITNESS_TARGET]", "java.lang.AssertionError@@@Main@@Main.java@@7"); - App.main(new String[] { - Base64.getEncoder().encodeToString(markers.getBytes(StandardCharsets.UTF_8)), - "--input", task.toString(), - "--property", prp.toString(), - "--out", out.toString(), - "--producer-version", "test" - }); - - String yaml = Files.readString(out.resolve("witness.yml")); - assertTrue(yaml.startsWith("- entry_type: \"violation_sequence\"\n")); - assertTrue(yaml.contains(" format_version: \"2.3\"\n")); - assertTrue(yaml.contains(" name: \"SWAT\"\n version: \"test\"\n")); - assertTrue(yaml.contains(" - \"" + task + "/Main.java\"\n")); - assertTrue(yaml.contains(" specification: \"CHECK( init(Main.main()), LTL(G assert) )\"\n")); - assertTrue(yaml.contains(" language: \"Java\"\n")); - assertFalse(yaml.contains("data_model")); - assertTrue(yaml.contains(" value: \"\\\\result == 3\"\n format: \"java_expression\"\n")); - assertTrue(yaml.contains(" line: 4\n column: 32\n")); - assertTrue(yaml.contains(" value: \"\\\\result.equals(\\\"a\\\\\\\"b\\\")\"\n")); - assertTrue(yaml.contains(" line: 5\n column: 38\n")); - assertTrue(yaml.contains(" type: \"target\"\n action: \"follow\"\n location:\n")); - assertTrue(yaml.contains(" line: 7\n column: 7\n")); - assertEquals(3, yaml.split(" - segment:\n").length - 1); - - String graphml = Files.readString(out.resolve("witness.graphml")); - assertTrue(graphml.contains("x = 3")); - assertTrue(graphml.contains("s.equals(\"a\\\"b\")")); - assertTrue(graphml.contains("true")); - } - - @Test - void defaultViolationWithoutMarkers(@TempDir Path dir) throws IOException { - Path task = dir.resolve("task"); - Files.createDirectories(task); - Files.writeString(task.resolve("Main.java"), "public class Main {\n public static void main(String[] a) {\n assert false;\n }\n}\n"); - Path out = dir.resolve("out"); - - App.main(new String[] {"", "--input", task.toString(), "--out", out.toString()}); - String yaml = Files.readString(out.resolve("witness.yml")); - assertTrue(yaml.contains(" content: []\n")); - assertTrue(Files.readString(out.resolve("witness.graphml")).contains("true")); - - // A target alone is enough for a final segment - App.main(new String[] { - Base64.getEncoder().encodeToString( - marker("[WITNESS_TARGET]", "java.lang.AssertionError@@@Main@@Main.java@@3").getBytes()), - "--input", task.toString(), "--out", out.toString() - }); - yaml = Files.readString(out.resolve("witness.yml")); - assertTrue(yaml.contains(" type: \"target\"\n")); - assertTrue(yaml.contains(" line: 3\n column: 5\n")); - } - - @Test - void legacyInvocationStillWorks(@TempDir Path dir) throws IOException { - Files.writeString(dir.resolve("Main.java"), "class Main {\n int x = Verifier.nondetInt();\n}\n"); - String markers = marker("[WITNESS]", "1@@@2@@@Main@@@()V"); - App.main(new String[] {Base64.getEncoder().encodeToString(markers.getBytes()), dir.toString()}); - String yaml = Files.readString(dir.resolve("witness.yml")); - assertTrue(yaml.contains(" value: \"\\\\result == 1\"\n")); - assertTrue(yaml.contains(" line: 2\n column: 30\n")); - assertTrue(Files.readString(dir.resolve("witness.graphml")).contains("<init>")); - } - - private static String marker(String kind, String payload) { - return "prefix " + kind + " " + Base64.getEncoder().encodeToString(payload.getBytes(StandardCharsets.UTF_8)); - } -} diff --git a/src/test/java/witness/JavaLiteralsTest.java b/src/test/java/witness/JavaLiteralsTest.java deleted file mode 100644 index 72cfc62..0000000 --- a/src/test/java/witness/JavaLiteralsTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package witness; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -class JavaLiteralsTest { - - @Test - void integralTypes() { - assertEquals("\\result == 5", JavaLiterals.resultConstraint("5", "I")); - assertEquals("\\result == -5", JavaLiterals.resultConstraint("-5", "I")); - assertEquals("\\result == -128", JavaLiterals.resultConstraint("-128", "B")); - assertEquals("\\result == 32767", JavaLiterals.resultConstraint("32767", "S")); - assertEquals("\\result == -2147483648", JavaLiterals.resultConstraint("-2147483648", "I")); - } - - @Test - void longsGetSuffixOnlyWhenNeeded() { - assertEquals("\\result == 5", JavaLiterals.resultConstraint("5", "J")); - assertEquals("\\result == 5000000000L", JavaLiterals.resultConstraint("5000000000", "J")); - assertEquals( - "\\result == -9223372036854775808L", - JavaLiterals.resultConstraint("-9223372036854775808", "J")); - } - - @Test - void booleans() { - assertEquals("\\result == true", JavaLiterals.resultConstraint("true", "Z")); - assertEquals("\\result == false", JavaLiterals.resultConstraint("false", "Z")); - } - - @Test - void charsAreRenderedAsLiterals() { - assertEquals("\\result == 'a'", JavaLiterals.resultConstraint("97", "C")); - assertEquals("\\result == '\\''", JavaLiterals.resultConstraint("39", "C")); - assertEquals("\\result == '\\\\'", JavaLiterals.resultConstraint("92", "C")); - assertEquals("\\result == '\\n'", JavaLiterals.resultConstraint("10", "C")); - assertEquals("\\result == '\\u0000'", JavaLiterals.resultConstraint("0", "C")); - assertEquals("\\result == '\\u00e4'", JavaLiterals.resultConstraint("228", "C")); - assertEquals("\\result == '\\uffff'", JavaLiterals.resultConstraint("65535", "C")); - } - - @Test - void floatingPoint() { - assertEquals("\\result == 1.5f", JavaLiterals.resultConstraint("1.5", "F")); - assertEquals("\\result == -0.000002022784f", JavaLiterals.resultConstraint("-0.000002022784", "F")); - assertEquals("\\result == 10000000000.0f", JavaLiterals.resultConstraint("1.0E10", "F")); - assertEquals("\\result == 0.1", JavaLiterals.resultConstraint("0.1", "D")); - assertEquals("\\result == -0.0", JavaLiterals.resultConstraint("-0.0", "D")); - assertEquals("\\result == 10000000000.0", JavaLiterals.resultConstraint("10000000000", "D")); - assertEquals("Float.isNaN(\\result)", JavaLiterals.resultConstraint("NaN", "F")); - assertEquals("Double.isNaN(\\result)", JavaLiterals.resultConstraint("NaN", "D")); - assertEquals("\\result == Double.POSITIVE_INFINITY", JavaLiterals.resultConstraint("Infinity", "D")); - assertEquals("\\result == Float.NEGATIVE_INFINITY", JavaLiterals.resultConstraint("-Infinity", "F")); - } - - @Test - void strings() { - assertEquals("\\result.equals(\"abc\")", JavaLiterals.resultConstraint("abc", "Ljava/lang/String;")); - assertEquals("\\result.equals(\"\")", JavaLiterals.resultConstraint("", "Ljava/lang/String;")); - assertEquals( - "\\result.equals(\"a\\\"b\\\\c\\nd\\te\")", - JavaLiterals.resultConstraint("a\"b\\c\nd\te", "Ljava/lang/String;")); - assertEquals( - "\\result.equals(\"\\u0001x\")", - JavaLiterals.resultConstraint("\u0001x", "Ljava/lang/String;")); - assertEquals("\\result.equals(\"ä€\")", JavaLiterals.resultConstraint("ä€", "Ljava/lang/String;")); - } - - @Test - void legacyMarkersWithoutType() { - assertEquals("\\result == 42", JavaLiterals.resultConstraint("42", null)); - assertEquals("\\result == true", JavaLiterals.resultConstraint("true", null)); - assertEquals("\\result.equals(\"hi\")", JavaLiterals.resultConstraint("hi", null)); - } -} diff --git a/src/test/java/witness/MarkerTest.java b/src/test/java/witness/MarkerTest.java deleted file mode 100644 index b9d4746..0000000 --- a/src/test/java/witness/MarkerTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package witness; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class MarkerTest { - - @Test - void parsesCurrentFormat() { - ValueMarker m = ValueMarker.parse("5@@@13@@@pkg/Main$1@@@main([Ljava/lang/String;)V@@@2@@@I@@@Main.java"); - assertEquals("5", m.getValue()); - assertEquals(13, m.getLine()); - assertEquals("pkg/Main$1", m.getClassName()); - assertEquals("main([Ljava/lang/String;)V", m.getMethodDesc()); - assertEquals(2, m.getCallIndex()); - assertEquals("I", m.getTypeDesc()); - assertEquals("Main.java", m.getSourceFile()); - } - - @Test - void stringValuesMayContainTheSeparator() { - ValueMarker m = ValueMarker.parse("a@@@b@@@7@@@Main@@@main()V@@@0@@@Ljava/lang/String;@@@Main.java"); - assertEquals("a@@@b", m.getValue()); - assertEquals(7, m.getLine()); - assertTrue(m.isString()); - - ValueMarker legacy = ValueMarker.parse("a@@@b@@@7@@@Main@@@main()V"); - assertEquals("a@@@b", legacy.getValue()); - assertEquals(7, legacy.getLine()); - } - - @Test - void parsesLegacyFormat() { - ValueMarker m = ValueMarker.parse("true@@@13@@@Main@@@main([Ljava/lang/String;)V"); - assertEquals("true", m.getValue()); - assertEquals(13, m.getLine()); - assertEquals("Main", m.getClassName()); - assertEquals(0, m.getCallIndex()); - assertNull(m.getTypeDesc()); - assertEquals("", m.getSourceFile()); - } - - @Test - void rejectsGarbage() { - assertThrows(IllegalArgumentException.class, () -> ValueMarker.parse("nonsense")); - } - - @Test - void parsesTarget() { - TargetMarker t = TargetMarker.parse( - "java.lang.AssertionError@@@Main@@Main.java@@23;pkg.Helper$Inner@@Helper.java@@5;java.lang.Thread@@@@-1"); - assertTrue(t.isAssertionError()); - assertEquals(3, t.getFrames().size()); - assertEquals("Main", t.getFrames().get(0).getClassName()); - assertEquals("Main.java", t.getFrames().get(0).getFileName()); - assertEquals(23, t.getFrames().get(0).getLine()); - assertEquals("", t.getFrames().get(2).getFileName()); - assertEquals(-1, t.getFrames().get(2).getLine()); - } - - @Test - void targetWithoutFrames() { - TargetMarker t = TargetMarker.parse("java.lang.RuntimeException@@@"); - assertEquals(0, t.getFrames().size()); - } -} diff --git a/src/witnesscreator/__init__.py b/src/witnesscreator/__init__.py new file mode 100644 index 0000000..fe8f234 --- /dev/null +++ b/src/witnesscreator/__init__.py @@ -0,0 +1,36 @@ +"""witnesscreator: SV-COMP violation witnesses for Java. + +>>> from witnesscreator import Task, Location, NondetReturn, Frame, Violation, write_witnesses +>>> task = Task(input_files=["../common/", "Main.java"], property_file="valid-assert.prp", +... producer_name="MyVerifier", producer_version="1.0") +>>> values = [NondetReturn("nondetInt", "int", "42", +... Location(class_name="Main", source_file="Main.java", line=12, call_index=0))] +>>> violation = Violation("java.lang.AssertionError", [Frame("Main", "Main.java", 17)]) +>>> write_witnesses(task, values, violation, out_dir="output") # doctest: +SKIP +{'yaml': PosixPath('output/witness.yml'), 'graphml': PosixPath('output/witness.graphml')} +""" + +from ._version import __version__ +from .events import dump_events, parse_events, read_events +from .markers import parse_marker_lines, parse_markers +from .model import JAVA_TYPES, Frame, Location, NondetReturn, Task, Violation +from .witness import FORMATS, render_graphml, render_yaml, write_witnesses + +__all__ = [ + "__version__", + "JAVA_TYPES", + "FORMATS", + "Task", + "Location", + "NondetReturn", + "Frame", + "Violation", + "write_witnesses", + "render_yaml", + "render_graphml", + "parse_markers", + "parse_marker_lines", + "read_events", + "parse_events", + "dump_events", +] diff --git a/src/witnesscreator/__main__.py b/src/witnesscreator/__main__.py new file mode 100644 index 0000000..dd8a8c9 --- /dev/null +++ b/src/witnesscreator/__main__.py @@ -0,0 +1,5 @@ +import sys + +from .cli import main + +sys.exit(main()) diff --git a/src/witnesscreator/_version.py b/src/witnesscreator/_version.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/src/witnesscreator/_version.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/src/witnesscreator/cli.py b/src/witnesscreator/cli.py new file mode 100644 index 0000000..6394ff4 --- /dev/null +++ b/src/witnesscreator/cli.py @@ -0,0 +1,164 @@ +"""Command line interface. + +:: + + witnesscreator events --events E.jsonl --property P.prp --input PATH... --out-dir D + [--format yaml|graphml|both] [--producer-name N] [--producer-version V] + witnesscreator legacy [ [

]] [--markers-file F] --input PATH... --property P.prp + --out-dir D [--format ...] [--producer-name N] [--producer-version V] + witnesscreator [] [--input PATH]... [--property P.prp] [--out D] + [--producer-version V] (the Java WitnessCreator's command line) + +Exit status 0 once a witness was written, 1 if nothing could be written, 2 for usage errors. +""" + +from __future__ import annotations + +import argparse +import logging +import sys +from pathlib import Path +from typing import List, Optional, Sequence + +from ._version import __version__ +from .events import read_events +from .markers import parse_markers +from .model import Task +from .witness import FORMATS, write_witnesses + +log = logging.getLogger("witnesscreator") + +SUBCOMMANDS = ("events", "legacy") +LEGACY_PRODUCER = "SWAT" + + +def _formats(choice: str) -> Sequence[str]: + return FORMATS if choice == "both" else (choice,) + + +def _add_common(p: argparse.ArgumentParser, producer_name: Optional[str], producer_version: Optional[str]) -> None: + p.add_argument("--input", action="append", default=[], metavar="PATH", + help="input file or directory of the task, exactly as given to the verifier (repeatable)") + p.add_argument("--property", metavar="FILE.prp", help="the property file; its content becomes the specification") + p.add_argument("--out-dir", "--out", dest="out_dir", metavar="DIR", help="where witness.yml / witness.graphml are written") + p.add_argument("--format", choices=("yaml", "graphml", "both"), default="both", help="which witness formats to write (default: both)") + p.add_argument("--producer-name", default=producer_name, metavar="NAME", + help=f"metadata.producer.name (default: {producer_name or 'the event file header, else witnesscreator'})") + p.add_argument("--producer-version", default=producer_version, metavar="VERSION", + help=f"metadata.producer.version (default: {producer_version or 'the event file header, else the witnesscreator version'})") + p.add_argument("--base-dir", metavar="DIR", help="resolve relative input and property paths against this directory") + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="witnesscreator", + description="Creates SV-COMP violation witnesses for Java (YAML format 2.3 and legacy GraphML).", + ) + parser.add_argument("--version", action="version", version=f"witnesscreator {__version__}") + parser.add_argument("-v", "--verbose", action="store_true", help="debug output") + parser.add_argument("-q", "--quiet", action="store_true", help="only warnings and errors") + sub = parser.add_subparsers(dest="command", metavar="COMMAND") + + events = sub.add_parser("events", help="create witnesses from an event file (JSON lines)") + events.add_argument("--events", required=True, metavar="FILE.jsonl", help="the event file") + _add_common(events, None, None) + + legacy = sub.add_parser( + "legacy", + help="create witnesses from SWAT's [WITNESS] / [WITNESS_TARGET] marker lines", + description="The markers are given base64-encoded on the command line (as SWAT printed them) " + "or as a file of marker lines. The positional is both an input and the default " + "output directory, like the Java WitnessCreator's.", + ) + legacy.add_argument("markers", nargs="?", default=None, metavar="BASE64", + help="base64 of the output lines carrying the markers (empty for the default violation witness)") + legacy.add_argument("dir", nargs="?", default=None, metavar="DIR", help="legacy: the task directory, also the output directory") + legacy.add_argument("--markers-file", metavar="FILE", help="file with the marker lines (plain text or base64)") + _add_common(legacy, LEGACY_PRODUCER, "unknown") + return parser + + +def _configure_logging(verbose: bool, quiet: bool) -> None: + level = logging.DEBUG if verbose else logging.WARNING if quiet else logging.INFO + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + logger = logging.getLogger("witnesscreator") + logger.handlers = [handler] + logger.setLevel(level) + logger.propagate = False + + +def _normalize_argv(argv: List[str]) -> List[str]: + """The Java command line (`` [] [--input ...]``) becomes ``legacy ...``.""" + if not argv: + return argv + first = argv[0] + if first in SUBCOMMANDS or first in ("-h", "--help", "--version"): + return argv + if first in ("-v", "--verbose", "-q", "--quiet") and len(argv) > 1 and argv[1] in SUBCOMMANDS: + return argv + return ["legacy"] + argv + + +def run_events(args: argparse.Namespace) -> int: + try: + ev = read_events(args.events) + except OSError as e: + log.error("Could not read event file %s: %s", args.events, e) + ev = None + values = ev.values if ev else [] + violation = ev.violation if ev else None + task = Task( + input_files=list(args.input), + property_file=args.property, + producer_name=args.producer_name or (ev.producer_name if ev else None) or "witnesscreator", + producer_version=args.producer_version or (ev.producer_version if ev else None) or __version__, + base_dir=args.base_dir, + ) + written = write_witnesses(task, values, violation, args.out_dir or ".", _formats(args.format)) + return 0 if written else 1 + + +def run_legacy(args: argparse.Namespace) -> int: + text = "" + if args.markers_file: + try: + text = Path(args.markers_file).read_text(encoding="utf-8", errors="replace") + except OSError as e: + log.error("Could not read markers file %s: %s", args.markers_file, e) + if args.markers: + text = (text + "\n" + args.markers) if text.strip() else args.markers + values, violation = parse_markers(text) + + inputs = list(args.input) + if not inputs and args.dir: + inputs.append(args.dir) + out_dir = args.out_dir or args.dir or "." + task = Task( + input_files=inputs, + property_file=args.property, + producer_name=args.producer_name or LEGACY_PRODUCER, + producer_version=args.producer_version or "unknown", + base_dir=args.base_dir, + ) + log.info("inputs: %s", inputs) + log.info("output directory: %s", out_dir) + written = write_witnesses(task, values, violation, out_dir, _formats(args.format)) + return 0 if written else 1 + + +def main(argv: Optional[Sequence[str]] = None) -> int: + argv = list(sys.argv[1:] if argv is None else argv) + parser = build_parser() + args = parser.parse_args(_normalize_argv(argv)) + _configure_logging(args.verbose, args.quiet) + if args.command == "events": + return run_events(args) + if args.command == "legacy": + return run_legacy(args) + parser.print_usage(sys.stderr) + return 2 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/src/witnesscreator/columns.py b/src/witnesscreator/columns.py new file mode 100644 index 0000000..a85546c --- /dev/null +++ b/src/witnesscreator/columns.py @@ -0,0 +1,126 @@ +"""Column heuristics over Java source lines. + +Columns are 1-based and count every character (a tab is one column), as the +witness format defines them. The heuristics work on lines whose comments and +string/char literals were blanked out by :func:`mask_non_code`, so a ``nondet(`` +inside a comment or a ``)`` inside a string does not confuse them. +""" + +from __future__ import annotations + +import re +from typing import Optional + +NONDET_CALL = re.compile(r"\bnondet[A-Za-z0-9_]*\s*\(") +ASSERT_KEYWORD = re.compile(r"\bassert\b") +THROW_KEYWORD = re.compile(r"\bthrow\b") + + +def _blank(s: str) -> str: + return "".join(ch if ch in "\r\n" else " " for ch in s) + + +def mask_non_code(text: str) -> str: + """Replaces comments and string/char literals by spaces, keeping every position. + + Line breaks are kept so that line numbers and columns of the result equal those + of the input. Text blocks (``\"\"\"``) and block comments may span lines. + """ + out = [] + i, n = 0, len(text) + while i < n: + c = text[i] + nxt = text[i + 1] if i + 1 < n else "" + if c == "/" and nxt == "/": + j = i + while j < n and text[j] not in "\r\n": + j += 1 + out.append(" " * (j - i)) + i = j + elif c == "/" and nxt == "*": + j = text.find("*/", i + 2) + j = n if j < 0 else j + 2 + out.append(_blank(text[i:j])) + i = j + elif c == '"' and text.startswith('"""', i): + j = text.find('"""', i + 3) + j = n if j < 0 else j + 3 + out.append(_blank(text[i:j])) + i = j + elif c in "\"'": + j = i + 1 + while j < n and text[j] != c and text[j] not in "\r\n": + if text[j] == "\\": + j += 1 + j += 1 + if j < n and text[j] == c: + j += 1 + out.append(_blank(text[i:j])) + i = j + else: + out.append(c) + i += 1 + return "".join(out) + + +def nondet_call_column(line: str, call_index: int) -> Optional[int]: + """The column of the closing parenthesis of the ``call_index``-th ``nondet*(`` call. + + ``line`` should be a masked line (see :func:`mask_non_code`). ``None`` if the + line has no such call or its parentheses are unbalanced. + """ + if line is None or call_index is None or call_index < 0: + return None + matches = list(NONDET_CALL.finditer(line)) + if call_index >= len(matches): + return None + open_paren = matches[call_index].end() - 1 + depth = 0 + for i in range(open_paren, len(line)): + ch = line[i] + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + return i + 1 + return None + + +def _is_java_identifier_part(ch: str) -> bool: + return ch.isalnum() or ch in "_$" + + +def _skip_whitespace(line: str, i: int) -> int: + while i < len(line) and line[i].isspace(): + i += 1 + return i + + +def statement_column(line: str, assertion: bool) -> Optional[int]: + """The column of the first character of the violating statement on the line. + + The ``assert`` of a failed assertion, the ``throw`` of an explicit throw, + otherwise the first token that is not a brace or an ``else``. ``line`` should + be a masked line (see :func:`mask_non_code`). + """ + if line is None: + return None + if assertion: + m = ASSERT_KEYWORD.search(line) + if m: + return m.start() + 1 + m = THROW_KEYWORD.search(line) + if m: + return m.start() + 1 + i = _skip_whitespace(line, 0) + while i < len(line): + if line[i] in "{}": + i = _skip_whitespace(line, i + 1) + elif line.startswith("else", i) and ( + i + 4 == len(line) or not _is_java_identifier_part(line[i + 4]) + ): + i = _skip_whitespace(line, i + 4) + else: + break + return i + 1 if i < len(line) else None diff --git a/src/witnesscreator/events.py b/src/witnesscreator/events.py new file mode 100644 index 0000000..bb68911 --- /dev/null +++ b/src/witnesscreator/events.py @@ -0,0 +1,166 @@ +"""The event file: JSON lines carrying the model objects. + +:: + + {"kind": "header", "schema": "witnesscreator-events", "version": 1, "producer": {"name": "SWAT", "version": "svcomp27"}} + {"kind": "nondet_return", "function": "nondetInt", "type": "int", "value": "42", "location": {...}, "bits": null} + {"kind": "violation", "throwable": "java.lang.AssertionError", "frames": [{"class_name": "Main", "source_file": "Main.java", "line": 17}]} + +The ``producer`` of the header is optional. Unknown lines are skipped with a +warning; a later ``violation`` line replaces an earlier one. +""" + +from __future__ import annotations + +import json +import logging +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Union + +from .model import Frame, Location, NondetReturn, Violation + +log = logging.getLogger("witnesscreator") + +SCHEMA = "witnesscreator-events" +VERSION = 1 + + +def header_dict(producer_name: Optional[str] = None, producer_version: Optional[str] = None) -> Dict[str, Any]: + header: Dict[str, Any] = {"kind": "header", "schema": SCHEMA, "version": VERSION} + if producer_name is not None or producer_version is not None: + header["producer"] = {"name": producer_name, "version": producer_version} + return header + + +def nondet_return_to_dict(nr: NondetReturn) -> Dict[str, Any]: + d: Dict[str, Any] = {"kind": "nondet_return"} + d.update(asdict(nr)) + return d + + +def violation_to_dict(v: Violation) -> Dict[str, Any]: + d: Dict[str, Any] = {"kind": "violation"} + d.update(asdict(v)) + return d + + +def _int(value: Any, default: int) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _opt_str(value: Any) -> Optional[str]: + return None if value is None else str(value) + + +def location_from_dict(d: Any) -> Location: + if not isinstance(d, dict): + return Location() + column = d.get("column") + return Location( + class_name=_opt_str(d.get("class_name")), + source_file=_opt_str(d.get("source_file")), + line=_int(d.get("line"), 0), + call_index=_int(d.get("call_index"), 0), + file_name=_opt_str(d.get("file_name")), + column=None if column is None else _int(column, 0) or None, + method=_opt_str(d.get("method")), + ) + + +def nondet_return_from_dict(d: Dict[str, Any]) -> NondetReturn: + value = d.get("value") + return NondetReturn( + function=_opt_str(d.get("function")), + type=_opt_str(d.get("type")), + value="" if value is None else (value if isinstance(value, str) else json.dumps(value)), + location=location_from_dict(d.get("location")), + bits=_opt_str(d.get("bits")), + ) + + +def frame_from_dict(d: Any) -> Optional[Frame]: + if not isinstance(d, dict): + return None + return Frame(str(d.get("class_name") or ""), _opt_str(d.get("source_file")), _int(d.get("line"), -1)) + + +def violation_from_dict(d: Dict[str, Any]) -> Violation: + frames = [f for f in (frame_from_dict(x) for x in (d.get("frames") or [])) if f is not None] + return Violation(str(d.get("throwable") or "unknown"), frames) + + +@dataclass +class EventFile: + header: Dict[str, Any] = field(default_factory=dict) + values: List[NondetReturn] = field(default_factory=list) + violation: Optional[Violation] = None + + @property + def producer_name(self) -> Optional[str]: + p = self.header.get("producer") + return _opt_str(p.get("name")) if isinstance(p, dict) else None + + @property + def producer_version(self) -> Optional[str]: + p = self.header.get("producer") + return _opt_str(p.get("version")) if isinstance(p, dict) else None + + +def parse_events(lines: Iterable[str]) -> EventFile: + """Parses event lines leniently: bad lines are logged and skipped.""" + ev = EventFile() + for number, raw in enumerate(lines, 1): + line = raw.strip() + if not line: + continue + try: + d = json.loads(line) + except ValueError as e: + log.warning("Skipping event line %d (%s): %s", number, e, line[:120]) + continue + if not isinstance(d, dict): + log.warning("Skipping event line %d: not an object", number) + continue + kind = d.get("kind") + try: + if kind == "header": + if d.get("schema") != SCHEMA: + log.warning("Unexpected event schema %r (expected %r)", d.get("schema"), SCHEMA) + if _int(d.get("version"), VERSION) > VERSION: + log.warning("Event file version %s is newer than %s", d.get("version"), VERSION) + ev.header = d + elif kind == "nondet_return": + ev.values.append(nondet_return_from_dict(d)) + elif kind == "violation": + if ev.violation is not None: + log.info("Event line %d replaces an earlier violation", number) + ev.violation = violation_from_dict(d) + else: + log.warning("Skipping event line %d of unknown kind %r", number, kind) + except Exception as e: # noqa: BLE001 + log.warning("Skipping event line %d (%s)", number, e) + return ev + + +def read_events(path: Union[str, Path]) -> EventFile: + with open(path, "r", encoding="utf-8", errors="replace") as fh: + return parse_events(fh) + + +def dump_events( + values: Iterable[NondetReturn], + violation: Optional[Violation], + producer_name: Optional[str] = None, + producer_version: Optional[str] = None, +) -> str: + """The event file text for the given objects (header first).""" + out = [json.dumps(header_dict(producer_name, producer_version))] + for nr in values: + out.append(json.dumps(nondet_return_to_dict(nr))) + if violation is not None: + out.append(json.dumps(violation_to_dict(violation))) + return "\n".join(out) + "\n" diff --git a/src/witnesscreator/graphml_writer.py b/src/witnesscreator/graphml_writer.py new file mode 100644 index 0000000..7dbcc77 --- /dev/null +++ b/src/witnesscreator/graphml_writer.py @@ -0,0 +1,122 @@ +"""Emitter for the legacy GraphML violation witness (format 1.0), as wit4java consumes it.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + +_XML_ESCAPES = {"&": "&", "<": "<", ">": ">", '"': """} + + +def _xml_char_ok(c: int) -> bool: + return c in (0x9, 0xA, 0xD) or 0x20 <= c <= 0xD7FF or 0xE000 <= c <= 0xFFFD or 0x10000 <= c <= 0x10FFFF + + +def xml_escape(s: str) -> str: + """Escapes markup characters; characters XML 1.0 cannot carry become ``\\uXXXX``.""" + out = [] + for ch in str(s): + esc = _XML_ESCAPES.get(ch) + if esc is not None: + out.append(esc) + elif _xml_char_ok(ord(ch)): + out.append(ch) + else: + out.append(f"\\u{ord(ch):04x}") + return "".join(out) + + +@dataclass +class Assumption: + """One edge of the witness automaton: a value a ``nondet*`` call returned.""" + + origin_file: str + line: int + text: str + scope: str + + +_HEADER = """ + + + <command-line> + + + + + false + + + false + + + false + + + false + + + false + + + 0 + + + 0 + + + + + + + + + + + + + + + + + violation_witness + Java +""" + + +def render_graphml(producer: str, specification: Optional[str], assumptions: List[Assumption]) -> str: + """The witness automaton: a chain of edges, one per assumption, ending in the violation node. + + Without assumptions the automaton is the single entry node that is also the + violation node (the "default violation"). + """ + out = [_HEADER] + out.append(f" {xml_escape(producer)}\n") + if specification: + out.append(f" {xml_escape(specification)}\n") + out.append(' \n') + if not assumptions: + out.append(' \n') + out.append(' true\n') + out.append(' true\n') + out.append(" \n") + else: + last = len(assumptions) + for i in range(last + 1): + out.append(f' \n') + if i == 0: + out.append(' true\n') + if i == last: + out.append(' true\n') + out.append(" \n") + for i, a in enumerate(assumptions): + out.append(f' \n') + out.append(f' {xml_escape(a.origin_file)}\n') + out.append(f' {int(a.line)}\n') + out.append(' 0\n') + out.append(f' {xml_escape(a.text)}\n') + out.append(f' {xml_escape(a.scope)}\n') + out.append(" \n") + out.append(" \n") + out.append("\n") + return "".join(out) diff --git a/src/witnesscreator/literals.py b/src/witnesscreator/literals.py new file mode 100644 index 0000000..c9930a5 --- /dev/null +++ b/src/witnesscreator/literals.py @@ -0,0 +1,209 @@ +"""Rendering of recorded values as Java literals and ``java_expression`` constraints.""" + +from __future__ import annotations + +import re +import struct +from decimal import Decimal, InvalidOperation +from typing import Optional + +from .model import normalize_type + +RESULT = "\\result" + +_INT_RE = re.compile(r"^[+-]?\d+$") +_INT_MIN, _INT_MAX = -(2**31), 2**31 - 1 + +_JAVA_ESCAPES = { + '"': '\\"', + "\\": "\\\\", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\b": "\\b", + "\f": "\\f", +} + +_CHAR_ESCAPES = { + 0x27: "'\\''", + 0x5C: "'\\\\'", + 0x0A: "'\\n'", + 0x0D: "'\\r'", + 0x09: "'\\t'", + 0x08: "'\\b'", + 0x0C: "'\\f'", +} + + +def result_constraint(value: str, type_name: Optional[str]) -> str: + """The ``java_expression`` satisfied exactly when the call returned ``value``. + + ``type_name`` is a Java type name (``int``, ``String``, ...) or a JVM descriptor + (``I``, ``Ljava/lang/String;``); ``None`` selects the legacy rendering that only + looks at the value (integers and booleans compare with ``==``, everything else is + a string). + """ + value = "" if value is None else str(value) + t = normalize_type(type_name) + if t is None: + return _legacy_constraint(value) + if t == "boolean": + return f"{RESULT} == {'true' if value.strip() == 'true' else 'false'}" + if t in ("byte", "short", "int"): + return f"{RESULT} == {value.strip()}" + if t == "long": + return f"{RESULT} == {long_literal(value)}" + if t == "char": + return f"{RESULT} == {char_literal(value)}" + if t == "float": + return floating_constraint(value, True) + if t == "double": + return floating_constraint(value, False) + return f"{RESULT}.equals({string_literal(value)})" + + +def _legacy_constraint(value: str) -> str: + v = value.strip() + if _INT_RE.match(v) or v in ("true", "false"): + return f"{RESULT} == {v}" + return f"{RESULT}.equals({string_literal(value)})" + + +def long_literal(value: str) -> str: + """``L`` suffix only when the value does not fit into an ``int``.""" + v = value.strip() + try: + n = int(v) + except ValueError: + return v + if _INT_MIN <= n <= _INT_MAX: + return v + return f"{v}L" + + +def char_literal(code_point) -> str: + """Char values are recorded as their code point; renders the Java char literal.""" + try: + c = int(str(code_point).strip()) + except ValueError: + return str(code_point) + if c < 0 or c > 0xFFFF: + return str(code_point) + if c in _CHAR_ESCAPES: + return _CHAR_ESCAPES[c] + if 0x20 <= c < 0x7F: + return f"'{chr(c)}'" + return f"'\\u{c:04x}'" + + +def string_literal(value: str) -> str: + """A Java string literal: every character outside printable ASCII is escaped. + + Characters outside the Basic Multilingual Plane are written as surrogate pairs, + which is how Java sees them. + """ + out = ['"'] + for ch in value: + esc = _JAVA_ESCAPES.get(ch) + if esc is not None: + out.append(esc) + continue + c = ord(ch) + if 0x20 <= c < 0x7F: + out.append(ch) + elif c > 0xFFFF: + c -= 0x10000 + out.append(f"\\u{0xD800 + (c >> 10):04x}\\u{0xDC00 + (c & 0x3FF):04x}") + else: + out.append(f"\\u{c:04x}") + out.append('"') + return "".join(out) + + +def plain_decimal(value: str) -> str: + """``1.0E10`` -> ``10000000000``, ``-0.0`` stays ``-0.0``; unparsable values are kept.""" + v = value.strip() + if v.endswith(("f", "F", "d", "D")) and not v.lower().endswith("infinity"): + v = v[:-1] + negative = v.startswith("-") + body = v[1:] if negative else v + try: + body = format(Decimal(body), "f") + except (InvalidOperation, ValueError): + return v + if body.startswith("-"): # Decimal("-0") style input after sign removal cannot happen, be safe + body = body[1:] + return f"-{body}" if negative else body + + +def floating_constraint(value: str, is_float: bool) -> str: + box = "Float" if is_float else "Double" + v = value.strip() + if v == "NaN": + return f"{box}.isNaN({RESULT})" + if v == "Infinity": + return f"{RESULT} == {box}.POSITIVE_INFINITY" + if v == "-Infinity": + return f"{RESULT} == {box}.NEGATIVE_INFINITY" + literal = plain_decimal(v) + if "." not in literal: + literal += ".0" + return f"{RESULT} == {literal}{'f' if is_float else ''}" + + +def value_from_bits(bits: str, type_name: Optional[str]) -> Optional[str]: + """The Java ``toString`` form of a float/double given its IEEE bits (``0x3dcccccd``).""" + t = normalize_type(type_name) + if t not in ("float", "double"): + return None + try: + n = int(str(bits).strip(), 0) + except ValueError: + return None + try: + if t == "float": + f = struct.unpack(">f", struct.pack(">I", n & 0xFFFFFFFF))[0] + else: + f = struct.unpack(">d", struct.pack(">Q", n & 0xFFFFFFFFFFFFFFFF))[0] + except struct.error: + return None + if f != f: + return "NaN" + if f in (float("inf"), float("-inf")): + return "Infinity" if f > 0 else "-Infinity" + if f == 0.0: + return "-0.0" if str(f).startswith("-") else "0.0" + return repr(f) + + +def graphml_assumption_value(value: str, type_name: Optional[str]) -> str: + """The right-hand side of a GraphML assumption, as wit4java's harness parses it. + + Primitives are plain decimal values that ``Integer.parseInt``/``Float.parseFloat`` + accept (chars as their code point, no ``L``/``f`` suffixes); strings become + ``.equals("")``. + """ + value = "" if value is None else str(value) + t = normalize_type(type_name) + if t is None: + v = value.strip() + if _INT_RE.match(v) or v in ("true", "false"): + return v + return f".equals({string_literal(value)})" + if t == "String": + return f".equals({string_literal(value)})" + if t == "boolean": + return "true" if value.strip() == "true" else "false" + if t == "char": + v = value.strip() + if _INT_RE.match(v): + return v + if len(v) == 1: + return str(ord(v)) + return v + if t in ("float", "double"): + v = value.strip() + if v in ("NaN", "Infinity", "-Infinity"): + return v + return plain_decimal(v) + return value.strip() diff --git a/src/witnesscreator/markers.py b/src/witnesscreator/markers.py new file mode 100644 index 0000000..a0c60d5 --- /dev/null +++ b/src/witnesscreator/markers.py @@ -0,0 +1,152 @@ +"""The legacy ``[WITNESS]`` / ``[WITNESS_TARGET]`` marker lines SWAT printed. + +Each marker is followed by the base64 of its payload: + +* ``[WITNESS]``: ``value@@@line@@@class@@@methodDesc@@@callIndex@@@typeDesc@@@sourceFile`` + (the 4-field form ``value@@@line@@@class@@@methodDesc`` is still accepted); + because a String value may contain the separator, fields are parsed from the end; +* ``[WITNESS_TARGET]``: ``throwable@@@class@@file@@line;class@@file@@line;...`` + (innermost frame first). +""" + +from __future__ import annotations + +import base64 +import binascii +import logging +import re +from typing import Iterable, List, Optional, Tuple + +from .model import Frame, Location, NondetReturn, Violation, normalize_type + +log = logging.getLogger("witnesscreator") + +VALUE_MARKER = "[WITNESS]" +TARGET_MARKER = "[WITNESS_TARGET]" +SEPARATOR = "@@@" +TYPE_DESCRIPTORS = {"Z", "B", "C", "S", "I", "J", "F", "D", "Ljava/lang/String;"} + +_B64_JUNK = re.compile(r"[^A-Za-z0-9+/=_-]") + + +def decode_base64(text: str) -> str: + """Decodes base64 (standard or URL-safe, padding optional) to UTF-8 text.""" + s = _B64_JUNK.sub("", str(text).strip()).replace("-", "+").replace("_", "/") + s += "=" * (-len(s) % 4) + try: + raw = base64.b64decode(s, validate=False) + except (binascii.Error, ValueError) as e: + raise ValueError(f"invalid base64: {e}") from e + return raw.decode("utf-8", errors="replace") + + +def _is_int(s: str) -> bool: + try: + int(s.strip()) + return True + except ValueError: + return False + + +def function_for_type(type_name: Optional[str]) -> Optional[str]: + t = normalize_type(type_name) + if t is None: + return None + return "nondet" + t[0].upper() + t[1:] + + +def parse_value_marker(payload: str) -> NondetReturn: + """Parses the payload of a ``[WITNESS]`` marker (7- or 4-field form).""" + parts = payload.split(SEPARATOR) + n = len(parts) + if n >= 7 and parts[n - 2].strip() in TYPE_DESCRIPTORS and _is_int(parts[n - 3]) and _is_int(parts[n - 6]): + type_desc = parts[n - 2].strip() + return NondetReturn( + function=function_for_type(type_desc), + type=normalize_type(type_desc), + value=SEPARATOR.join(parts[: n - 6]), + location=Location( + class_name=parts[n - 5].strip(), + source_file=parts[n - 1].strip() or None, + line=int(parts[n - 6].strip()), + call_index=int(parts[n - 3].strip()), + method=parts[n - 4].strip() or None, + ), + ) + if n >= 4 and _is_int(parts[n - 3]): + return NondetReturn( + function=None, + type=None, + value=SEPARATOR.join(parts[: n - 3]), + location=Location( + class_name=parts[n - 2].strip(), + source_file=None, + line=int(parts[n - 3].strip()), + call_index=0, + method=parts[n - 1].strip() or None, + ), + ) + raise ValueError(f"invalid witness marker: {payload!r}") + + +def parse_target_marker(payload: str) -> Violation: + """Parses the payload of a ``[WITNESS_TARGET]`` marker.""" + idx = payload.find(SEPARATOR) + if idx < 0: + raise ValueError(f"invalid witness target marker: {payload!r}") + throwable = payload[:idx].strip() + frames: List[Frame] = [] + rest = payload[idx + len(SEPARATOR) :].strip() + if rest: + for frame in rest.split(";"): + fields = frame.split("@@") + if len(fields) != 3: + continue + try: + line = int(fields[2].strip()) + except ValueError: + line = -1 + frames.append(Frame(fields[0].strip(), fields[1].strip() or None, line)) + return Violation(throwable, frames) + + +def parse_marker_lines(lines: Iterable[str]) -> Tuple[List[NondetReturn], Optional[Violation]]: + """Extracts the values and the (last) target from output lines carrying markers. + + Lines without a marker are ignored, so the whole output of a run may be passed. + Undecodable markers are logged and skipped. + """ + values: List[NondetReturn] = [] + violation: Optional[Violation] = None + for raw in lines: + line = str(raw) + idx = line.find(TARGET_MARKER) + if idx >= 0: + try: + violation = parse_target_marker(decode_base64(line[idx + len(TARGET_MARKER) :])) + except ValueError as e: + log.warning("Ignoring bad target marker (%s): %s", e, line.strip()) + continue + idx = line.find(VALUE_MARKER) + if idx >= 0: + try: + values.append(parse_value_marker(decode_base64(line[idx + len(VALUE_MARKER) :]))) + except ValueError as e: + log.warning("Ignoring bad witness marker (%s): %s", e, line.strip()) + return values, violation + + +def parse_markers(text: str) -> Tuple[List[NondetReturn], Optional[Violation]]: + """Like :func:`parse_marker_lines` for a block of text, which may itself be base64.""" + if text is None: + return [], None + if VALUE_MARKER not in text and TARGET_MARKER not in text and text.strip(): + try: + decoded = decode_base64(text) + except ValueError: + decoded = "" + if VALUE_MARKER in decoded or TARGET_MARKER in decoded: + text = decoded + elif decoded.strip(): + log.warning("No witness markers found in the (decoded) input") + return parse_marker_lines(text.splitlines()) diff --git a/src/witnesscreator/model.py b/src/witnesscreator/model.py new file mode 100644 index 0000000..82c4557 --- /dev/null +++ b/src/witnesscreator/model.py @@ -0,0 +1,125 @@ +"""The data model shared by the library, the event file and the command line. + +Every object is a plain dataclass; ``write_witnesses`` never mutates them. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional, Sequence, Union + +from ._version import __version__ + +#: Java types of ``Verifier.nondet*()`` results, as spelled in :attr:`NondetReturn.type`. +JAVA_TYPES = ("boolean", "byte", "short", "char", "int", "long", "float", "double", "String") + +_DESCRIPTOR_TO_TYPE = { + "Z": "boolean", + "B": "byte", + "S": "short", + "C": "char", + "I": "int", + "J": "long", + "F": "float", + "D": "double", + "Ljava/lang/String;": "String", + "java.lang.String": "String", + "java/lang/String": "String", + "string": "String", +} + + +def normalize_type(type_name: Optional[str]) -> Optional[str]: + """Maps a Java type name or JVM descriptor to one of :data:`JAVA_TYPES`. + + Returns ``None`` for an unknown or missing type, which selects the legacy + rendering that only looks at the value. + """ + if type_name is None: + return None + t = str(type_name).strip() + if t in JAVA_TYPES: + return t + return _DESCRIPTOR_TO_TYPE.get(t) + + +@dataclass +class Location: + """Where a ``Verifier.nondet*()`` call happened. + + Two forms are accepted: + + * ``Location(class_name="pkg.Main", source_file="Main.java", line=12, call_index=0)``: + the call site is resolved to one of the task's source files and the column is + derived from the source text; + * ``Location(file_name="Main.java", line=12, column=27)``: the producer already + knows the file (one of the task's input files) and, optionally, the column. + + ``method`` optionally names the calling method (``main([Ljava/lang/String;)V``); + it only appears in the GraphML ``assumption.scope``. + """ + + class_name: Optional[str] = None + source_file: Optional[str] = None + line: int = 0 + call_index: int = 0 + file_name: Optional[str] = None + column: Optional[int] = None + method: Optional[str] = None + + +@dataclass +class NondetReturn: + """One value returned by a ``Verifier.nondet*()`` call. + + ``value`` is the string form of the value: integers in decimal, ``char`` as its + code point in decimal, ``boolean`` as ``true``/``false``, ``float``/``double`` as + Java's ``Float.toString``/``Double.toString`` (``NaN``, ``Infinity``, + ``-Infinity``, ``-0.0`` included) and ``String`` as the raw, unescaped string. + """ + + function: Optional[str] + type: Optional[str] + value: str + location: Location + bits: Optional[str] = None + + +@dataclass +class Frame: + """A stack frame of the uncaught throwable, as ``StackTraceElement`` reports it.""" + + class_name: str + source_file: Optional[str] = None + line: int = -1 + + +@dataclass +class Violation: + """The uncaught throwable that ended the violating run, innermost frame first.""" + + throwable: str + frames: list = field(default_factory=list) + + @property + def is_assertion_error(self) -> bool: + return self.throwable == "java.lang.AssertionError" + + +@dataclass +class Task: + """The verification task the witness belongs to. + + ``input_files`` are the files or directories exactly as given to the verifier; + directories are expanded to the ``.java`` files they contain. ``property_file`` + is the ``.prp`` file whose content becomes the witness' specification. + Relative input paths are resolved against ``base_dir`` (default: the current + working directory) but written to the witness as given. + """ + + input_files: Sequence[str] = field(default_factory=list) + property_file: Union[str, Path, None] = None + producer_name: str = "witnesscreator" + producer_version: str = __version__ + base_dir: Union[str, Path, None] = None diff --git a/src/witnesscreator/sources.py b/src/witnesscreator/sources.py new file mode 100644 index 0000000..bf20b18 --- /dev/null +++ b/src/witnesscreator/sources.py @@ -0,0 +1,263 @@ +"""The Java source files of a verification task and the lookup of classes in them.""" + +from __future__ import annotations + +import hashlib +import logging +import os +import re +from pathlib import Path +from typing import Callable, Iterable, List, Optional, Union + +from .columns import mask_non_code + +log = logging.getLogger("witnesscreator") + +_LINE_BREAK = re.compile(r"\r\n|\r|\n") +_PACKAGE_DECL = re.compile(r"^\s*package\s+([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*;", re.MULTILINE) +_UNSET = object() + + +def to_slashes(s: str) -> str: + return str(s).replace("\\", "/") + + +def strip_trailing_slash(s: str) -> str: + r = to_slashes(s) + while len(r) > 1 and r.endswith("/"): + r = r[:-1] + return r + + +def split_lines(text: str) -> List[str]: + """Splits on ``\\n``, ``\\r\\n`` and ``\\r`` only (like ``Files.readAllLines``).""" + if text == "": + return [] + lines = _LINE_BREAK.split(text) + if lines and lines[-1] == "": + lines.pop() + return lines + + +class InputFile: + """One ``.java`` file of the task.""" + + def __init__(self, display_path: str, path: Path, relative_path: str): + #: The path as written into the witness: the input as given plus the relative path. + self.display_path = display_path + #: Where the file is read from. + self.path = path + #: Path relative to the input root it was found in, ``/``-separated. + self.relative_path = relative_path + self._lines: Optional[List[str]] = None + self._masked: Optional[List[str]] = None + self._sha256: Optional[str] = None + self._package = _UNSET + + @property + def file_name(self) -> str: + return self.path.name + + def _read(self) -> str: + try: + return self.path.read_bytes().decode("utf-8", errors="replace") + except OSError as e: + log.warning("Could not read %s: %s", self.path, e) + return "" + + @property + def lines(self) -> List[str]: + if self._lines is None: + self._lines = split_lines(self._read()) + return self._lines + + @property + def masked_lines(self) -> List[str]: + """The lines with comments and string/char literals blanked out.""" + if self._masked is None: + self._masked = split_lines(mask_non_code("\n".join(self.lines) + "\n")) + while len(self._masked) < len(self.lines): + self._masked.append("") + return self._masked + + @property + def package(self) -> Optional[str]: + """The declared package (``""`` for the default package), ``None`` if unreadable.""" + if self._package is _UNSET: + if not self.lines: + self._package = None + else: + m = _PACKAGE_DECL.search("\n".join(self.masked_lines)) + self._package = m.group(1) if m else "" + return self._package + + def line(self, number: int) -> Optional[str]: + """The 1-based line, or ``None`` if the file does not have it.""" + if number < 1 or number > len(self.lines): + return None + return self.lines[number - 1] + + def masked_line(self, number: int) -> Optional[str]: + if number < 1 or number > len(self.lines): + return None + return self.masked_lines[number - 1] + + @property + def sha256(self) -> str: + if self._sha256 is None: + try: + self._sha256 = hashlib.sha256(self.path.read_bytes()).hexdigest() + except OSError as e: + log.warning("Could not hash %s: %s", self.path, e) + self._sha256 = "" + return self._sha256 + + def __repr__(self) -> str: + return f"InputFile({self.display_path!r})" + + +class SourceFiles: + """The ``.java`` files of the task, in the order of the inputs.""" + + def __init__(self, files: List[InputFile]): + self.files = list(files) + + def __len__(self) -> int: + return len(self.files) + + def __iter__(self): + return iter(self.files) + + @classmethod + def collect(cls, inputs: Iterable[str], base_dir: Union[str, Path, None] = None) -> "SourceFiles": + """Collects the ``.java`` files of the inputs (files or directories, searched recursively). + + A file reachable through several inputs is listed once. Relative inputs are + resolved against ``base_dir`` but displayed as given. + """ + base = Path(base_dir) if base_dir is not None else None + files: List[InputFile] = [] + seen = set() + for given in inputs: + given = str(given) + if given == "": + continue + root = Path(given) + if base is not None and not root.is_absolute(): + root = base / root + try: + if root.is_dir(): + prefix = strip_trailing_slash(given) + found = [] + for dirpath, dirnames, filenames in os.walk(root): + dirnames.sort() + for name in filenames: + if name.endswith(".java"): + found.append(Path(dirpath) / name) + found.sort(key=lambda p: to_slashes(str(p))) + for p in found: + key = os.path.normpath(os.path.abspath(p)) + if key in seen: + continue + seen.add(key) + rel = to_slashes(str(p.relative_to(root))) + files.append(InputFile(f"{prefix}/{rel}", p, rel)) + elif root.is_file(): + if not root.name.endswith(".java"): + log.info("Ignoring input that is not a Java source: %s", given) + continue + key = os.path.normpath(os.path.abspath(root)) + if key in seen: + continue + seen.add(key) + files.append(InputFile(to_slashes(given), root, root.name)) + else: + log.warning("Ignoring missing input: %s", given) + except OSError as e: + log.warning("Could not read input %s: %s", given, e) + return cls(files) + + def _find(self, predicate: Callable[[InputFile], bool]) -> Optional[InputFile]: + for f in self.files: + if predicate(f): + return f + return None + + def resolve( + self, class_name: str, source_file_name: Optional[str], allow_name_only: bool = True + ) -> Optional[InputFile]: + """Finds the source file of a class. + + ``class_name`` may be JVM internal (``pkg/Outer$Inner``) or dotted + (``pkg.Outer$Inner``); ``source_file_name`` is the class' ``SourceFile`` + attribute, if known. + + Candidates are the files of the expected name; a candidate whose ``package`` + declaration contradicts the class' package is never taken. Among the rest, + the file whose path inside its input matches the package path wins (exact + match first, then as a suffix). A file whose package declaration matches is + accepted regardless of its path; a file whose declaration could not be read + only with ``allow_name_only``. + """ + internal = str(class_name).replace(".", "/") + slash = internal.rfind("/") + pkg_dir = "" if slash < 0 else internal[:slash] + package = pkg_dir.replace("/", ".") + simple = internal if slash < 0 else internal[slash + 1 :] + dollar = simple.find("$") + outer = simple if dollar < 0 else simple[:dollar] + + names: List[str] = [] + if source_file_name: + names.append(to_slashes(source_file_name).rsplit("/", 1)[-1]) + if f"{outer}.java" not in names: + names.append(f"{outer}.java") + + for name in names: + candidates = [f for f in self.files if f.file_name == name and f.package in (package, None)] + if not candidates: + continue + rel = name if not pkg_dir else f"{pkg_dir}/{name}" + for f in candidates: + if f.relative_path == rel: + return f + for f in candidates: + if f.relative_path.endswith("/" + rel): + return f + for f in candidates: + if f.package == package: + return f + if allow_name_only: + return candidates[0] + return None + + def resolve_file_name(self, file_name: str) -> Optional[InputFile]: + """Finds an input file by the path a producer wrote (as given, relative, or by name).""" + name = to_slashes(file_name) + while name.startswith("./"): + name = name[2:] + if not name: + return None + for pred in ( + lambda f: f.display_path == name, + lambda f: f.relative_path == name, + lambda f: f.display_path.endswith("/" + name), + lambda f: f.relative_path.endswith("/" + name), + lambda f: f.file_name == name.rsplit("/", 1)[-1], + ): + match = self._find(pred) + if match is not None: + return match + return None + + +def fallback_file_name(class_name: str, source_file: Optional[str]) -> str: + """The file a class should live in when it was not found among the inputs.""" + internal = str(class_name).replace(".", "/") + slash = internal.rfind("/") + pkg_dir = "" if slash < 0 else internal[: slash + 1] + if source_file: + return pkg_dir + to_slashes(source_file).rsplit("/", 1)[-1] + simple = internal[slash + 1 :] + dollar = simple.find("$") + return pkg_dir + (simple if dollar < 0 else simple[:dollar]) + ".java" diff --git a/src/witnesscreator/witness.py b/src/witnesscreator/witness.py new file mode 100644 index 0000000..7470b8b --- /dev/null +++ b/src/witnesscreator/witness.py @@ -0,0 +1,283 @@ +"""Turns a task, the recorded values and the violation into witness documents.""" + +from __future__ import annotations + +import logging +import uuid +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union + +from .columns import nondet_call_column, statement_column +from .graphml_writer import Assumption, render_graphml as _render_graphml +from .literals import graphml_assumption_value, result_constraint, value_from_bits +from .model import Location, NondetReturn, Task, Violation +from .sources import InputFile, SourceFiles, fallback_file_name, to_slashes +from .yaml_writer import Metadata, Waypoint, render_yaml as _render_yaml + +log = logging.getLogger("witnesscreator") + +DEFAULT_SPECIFICATION = "CHECK( init(Main.main()), LTL(G assert) )" +FORMATS = ("yaml", "graphml") +FILE_NAMES = {"yaml": "witness.yml", "graphml": "witness.graphml"} + +#: Stack frames of these packages never are the target: the violating statement is +#: the innermost frame of the task sources that called into them. +SKIPPED_FRAME_PREFIXES = ("java.", "javax.", "jdk.", "sun.", "de.uzl.its.swat.") + + +@dataclass +class BuiltWitness: + """Everything the writers need, computed once for all formats.""" + + metadata: Metadata + waypoints: List[Waypoint] = field(default_factory=list) + assumptions: List[Assumption] = field(default_factory=list) + target: Optional[Waypoint] = None + + @property + def has_target(self) -> bool: + return self.target is not None + + +def read_specification(property_file: Union[str, Path, None], base_dir: Union[str, Path, None] = None) -> str: + """The content of the property file; the default specification if it cannot be read.""" + if property_file is None or str(property_file) == "": + log.warning("No property file given, assuming specification: %s", DEFAULT_SPECIFICATION) + return DEFAULT_SPECIFICATION + path = Path(property_file) + if base_dir is not None and not path.is_absolute(): + path = Path(base_dir) / path + try: + spec = path.read_bytes().decode("utf-8", errors="replace").strip() + except OSError as e: + log.warning("Could not read property file %s: %s", property_file, e) + spec = "" + if spec: + return spec + log.warning("Assuming specification: %s", DEFAULT_SPECIFICATION) + return DEFAULT_SPECIFICATION + + +def _scope(class_name: Optional[str], method: Optional[str], file_name: Optional[str]) -> str: + if class_name: + internal = str(class_name).replace(".", "/") + elif file_name: + internal = to_slashes(file_name).rsplit("/", 1)[-1] + if internal.endswith(".java"): + internal = internal[: -len(".java")] + else: + internal = "" + scope = f"java::L{internal};" + if method: + scope += f".{method}" + return scope + + +class _Builder: + def __init__(self, task: Task, sources: SourceFiles): + self.task = task + self.sources = sources + + def value(self, nr: NondetReturn) -> Tuple[Optional[Waypoint], Optional[Assumption]]: + loc = nr.location if nr.location is not None else Location() + value = nr.value + if (value is None or value == "") and nr.bits: + derived = value_from_bits(nr.bits, nr.type) + if derived is not None: + value = derived + value = "" if value is None else str(value) + try: + line = int(loc.line) + except (TypeError, ValueError): + line = 0 + if line < 1: + log.warning("Ignoring value %r without a valid line: %s", value, loc) + return None, None + + found: Optional[InputFile] = None + if loc.file_name: + found = self.sources.resolve_file_name(loc.file_name) + display = found.display_path if found else to_slashes(loc.file_name) + origin = found.relative_path if found else to_slashes(loc.file_name) + if found is None: + log.warning("%s is not among the input files, using it as given", loc.file_name) + elif loc.class_name: + found = self.sources.resolve(loc.class_name, loc.source_file, allow_name_only=True) + if found is not None: + display = found.display_path + origin = found.relative_path + else: + display = origin = fallback_file_name(loc.class_name, loc.source_file) + log.warning("Could not find the source of %s, using %s", loc.class_name, display) + else: + log.warning("Ignoring value %r without a file or class: %s", value, loc) + return None, None + + column = loc.column + if column is not None: + try: + column = int(column) + except (TypeError, ValueError): + column = None + if column is not None and column < 1: + column = None + if column is None and found is not None: + code = found.masked_line(line) + column = nondet_call_column(code, loc.call_index) if code is not None else None + if column is None: + log.warning( + "Could not locate nondet call %s in %s:%s (%r)", + loc.call_index, display, line, found.line(line), + ) + + constraint = result_constraint(value, nr.type) + waypoint = Waypoint.function_return(constraint, display, line, column) + scope = _scope(loc.class_name, loc.method, display) + rhs = graphml_assumption_value(value, nr.type) + text = f"\\result{rhs};" if rhs.startswith(".equals(") else f"\\result = {rhs};" + return waypoint, Assumption(origin, line, text, scope) + + def target(self, violation: Violation) -> Optional[Waypoint]: + for frame in violation.frames or []: + try: + line = int(frame.line) + except (TypeError, ValueError): + continue + if line < 1: + continue + class_name = str(frame.class_name or "").replace("/", ".") + if not class_name or class_name.startswith(SKIPPED_FRAME_PREFIXES): + continue + found = self.sources.resolve(class_name, frame.source_file, allow_name_only=False) + if found is None: + continue + code = found.masked_line(line) + if code is None: + log.warning("Frame %s:%s is outside of %s", class_name, line, found.path) + continue + column = statement_column(code, violation.is_assertion_error) + return Waypoint.target(found.display_path, line, column) + return None + + +def build_witness(task: Task, values: Iterable[NondetReturn], violation: Optional[Violation]) -> BuiltWitness: + """Resolves locations and renders constraints; never raises for bad values.""" + sources = SourceFiles.collect(task.input_files or [], task.base_dir) + specification = read_specification(task.property_file, task.base_dir) + metadata = Metadata( + uuid=str(uuid.uuid4()), + creation_time=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + producer_name=str(task.producer_name), + producer_version=str(task.producer_version), + specification=specification, + input_files=[(f.display_path, f.sha256) for f in sources], + ) + built = BuiltWitness(metadata) + builder = _Builder(task, sources) + + for nr in values or []: + try: + waypoint, assumption = builder.value(nr) + except Exception as e: # noqa: BLE001 - a bad value must not lose the witness + log.error("Ignoring value %r: %s", nr, e) + continue + if waypoint is not None: + built.waypoints.append(waypoint) + if assumption is not None: + built.assumptions.append(assumption) + if not built.waypoints: + log.info("No witness values recorded, assuming default violation") + + if violation is not None: + try: + built.target = builder.target(violation) + except Exception as e: # noqa: BLE001 + log.error("Could not resolve the target of %r: %s", violation, e) + if built.target is None: + log.warning( + "No stack frame of %s lies in the task sources, the witness has no target waypoint: %s", + violation.throwable, violation.frames, + ) + else: + built.waypoints.append(built.target) + else: + log.warning("No violation recorded, the witness has no target waypoint") + + for wp in built.waypoints: + log.info("Waypoint: %s", wp) + return built + + +def render_yaml(task: Task, values: Iterable[NondetReturn], violation: Optional[Violation]) -> str: + """The YAML (format 2.3) witness as a string.""" + built = build_witness(task, values, violation) + return _render_yaml(built.metadata, built.waypoints) + + +def render_graphml(task: Task, values: Iterable[NondetReturn], violation: Optional[Violation]) -> str: + """The legacy GraphML witness as a string.""" + built = build_witness(task, values, violation) + return _render_graphml(built.metadata.producer_name, built.metadata.specification, built.assumptions) + + +def _render(built: BuiltWitness, fmt: str) -> str: + if fmt == "yaml": + return _render_yaml(built.metadata, built.waypoints) + if fmt == "graphml": + return _render_graphml(built.metadata.producer_name, built.metadata.specification, built.assumptions) + raise ValueError(f"unknown witness format: {fmt}") + + +def write_witnesses( + task: Task, + values: Iterable[NondetReturn], + violation: Optional[Violation], + out_dir: Union[str, Path] = "output", + formats: Sequence[str] = FORMATS, +) -> Dict[str, Path]: + """Writes ``witness.yml`` and/or ``witness.graphml`` into ``out_dir``. + + Never raises for bad data: problems are logged (logger ``witnesscreator``) and + each format is written independently. Returns the files actually written, by + format. + """ + written: Dict[str, Path] = {} + wanted = [f for f in (formats or ()) if f in FORMATS] + for f in formats or (): + if f not in FORMATS: + log.error("Unknown witness format %r (known: %s)", f, ", ".join(FORMATS)) + if not wanted: + return written + + values = list(values or []) + try: + built = build_witness(task, values, violation) + except Exception as e: # noqa: BLE001 - last line of defence + log.error("Could not build the witness, writing a witness without waypoints: %s", e) + metadata = Metadata( + uuid=str(uuid.uuid4()), + creation_time=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + producer_name=str(getattr(task, "producer_name", "unknown")), + producer_version=str(getattr(task, "producer_version", "unknown")), + specification=DEFAULT_SPECIFICATION, + input_files=[], + ) + built = BuiltWitness(metadata) + + out = Path(out_dir) + try: + out.mkdir(parents=True, exist_ok=True) + except OSError as e: + log.error("Could not create output directory %s: %s", out, e) + for fmt in wanted: + path = out / FILE_NAMES[fmt] + try: + text = _render(built, fmt) + log.info("Writing violation witness to file: %s", path) + path.write_text(text, encoding="utf-8", newline="\n") + written[fmt] = path + except Exception as e: # noqa: BLE001 + log.error("Could not write %s witness to %s: %s", fmt, path, e) + return written diff --git a/src/witnesscreator/yaml_writer.py b/src/witnesscreator/yaml_writer.py new file mode 100644 index 0000000..226ed94 --- /dev/null +++ b/src/witnesscreator/yaml_writer.py @@ -0,0 +1,126 @@ +"""Hand-written emitter for violation witnesses in the YAML format, version 2.3.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional, Sequence, Tuple + +FORMAT_VERSION = "2.3" + +_YAML_ESCAPES = { + '"': '\\"', + "\\": "\\\\", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\x85": "\\N", + "
": "\\L", + "
": "\\P", +} + + +def yaml_quote(s: str) -> str: + """Renders a YAML double-quoted scalar. + + Everything outside printable ASCII is escaped (``\\uXXXX``, ``\\UXXXXXXXX`` for + characters beyond the BMP), so the output is plain ASCII that every YAML parser + decodes to the same string, including control characters and lone surrogates. + """ + out = ['"'] + for ch in str(s): + esc = _YAML_ESCAPES.get(ch) + if esc is not None: + out.append(esc) + continue + c = ord(ch) + if 0x20 <= c < 0x7F: + out.append(ch) + elif c > 0xFFFF: + out.append(f"\\U{c:08x}") + else: + out.append(f"\\u{c:04x}") + out.append('"') + return "".join(out) + + +@dataclass +class Waypoint: + """A ``follow`` waypoint of the single violation sequence.""" + + type: str + constraint: Optional[str] + file_name: str + line: int + column: Optional[int] = None + + @classmethod + def function_return(cls, constraint: str, file_name: str, line: int, column: Optional[int]): + return cls("function_return", constraint, file_name, line, column) + + @classmethod + def target(cls, file_name: str, line: int, column: Optional[int]): + return cls("target", None, file_name, line, column) + + def __str__(self) -> str: + s = f"{self.type} @ {self.file_name}:{self.line}" + if self.column is not None: + s += f":{self.column}" + if self.constraint is not None: + s += f" [{self.constraint}]" + return s + + +@dataclass +class Metadata: + uuid: str + creation_time: str + producer_name: str + producer_version: str + specification: str + input_files: Sequence[Tuple[str, str]] # (display path, sha256) + + +def render_yaml(metadata: Metadata, waypoints: List[Waypoint]) -> str: + """The witness document: one ``violation_sequence`` entry, one segment per waypoint.""" + q = yaml_quote + out = [] + out.append('- entry_type: "violation_sequence"\n') + out.append(" metadata:\n") + out.append(f" format_version: {q(FORMAT_VERSION)}\n") + out.append(f" uuid: {q(metadata.uuid)}\n") + out.append(f" creation_time: {q(metadata.creation_time)}\n") + out.append(" producer:\n") + out.append(f" name: {q(metadata.producer_name)}\n") + out.append(f" version: {q(metadata.producer_version)}\n") + out.append(" task:\n") + if not metadata.input_files: + out.append(" input_files: []\n") + out.append(" input_file_hashes: {}\n") + else: + out.append(" input_files:\n") + for path, _ in metadata.input_files: + out.append(f" - {q(path)}\n") + out.append(" input_file_hashes:\n") + for path, sha in metadata.input_files: + out.append(f" {q(path)}: {q(sha)}\n") + out.append(f" specification: {q(metadata.specification)}\n") + out.append(' language: "Java"\n') + if not waypoints: + out.append(" content: []\n") + return "".join(out) + out.append(" content:\n") + for wp in waypoints: + out.append(" - segment:\n") + out.append(" - waypoint:\n") + out.append(f" type: {q(wp.type)}\n") + out.append(' action: "follow"\n') + if wp.constraint is not None: + out.append(" constraint:\n") + out.append(f" value: {q(wp.constraint)}\n") + out.append(' format: "java_expression"\n') + out.append(" location:\n") + out.append(f" file_name: {q(wp.file_name)}\n") + out.append(f" line: {int(wp.line)}\n") + if wp.column is not None: + out.append(f" column: {int(wp.column)}\n") + return "".join(out) diff --git a/tools/build_pyz.py b/tools/build_pyz.py new file mode 100644 index 0000000..25eada6 --- /dev/null +++ b/tools/build_pyz.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Builds the single-file ``witnesscreator.pyz`` (a zipapp, stdlib only). + +usage: tools/build_pyz.py [OUTPUT] (default: dist/witnesscreator.pyz) +""" + +import shutil +import sys +import tempfile +import zipapp +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PACKAGE = ROOT / "src" / "witnesscreator" + + +def main() -> int: + target = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "dist" / "witnesscreator.pyz" + target.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory() as tmp: + staging = Path(tmp) / "app" + shutil.copytree(PACKAGE, staging / "witnesscreator", ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) + zipapp.create_archive( + staging, + target=target, + interpreter="/usr/bin/env python3", + main="witnesscreator.cli:main", + compressed=True, + ) + print(target) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/witnesses/default_violation.st b/witnesses/default_violation.st deleted file mode 100755 index 81cb4a6..0000000 --- a/witnesses/default_violation.st +++ /dev/null @@ -1,56 +0,0 @@ - - - - <command-line> - - - - - false - - - false - - - false - - - false - - - false - - - 0 - - - 0 - - - - - - - - - - - - - - - - - violation_witness - SWAT - - - - true true - - - \ No newline at end of file diff --git a/witnesses/witness.st b/witnesses/witness.st deleted file mode 100755 index 6e7e57a..0000000 --- a/witnesses/witness.st +++ /dev/null @@ -1,70 +0,0 @@ - - - - <command-line> - - - - - false - - - false - - - false - - - false - - - false - - - 0 - - - 0 - - - - - - - - - - - - - - - - - violation_witness - SWAT - - - - $nodes:{ n | - - $n.data.keys:{k | - $n.data.(k)$ }$ - - }$ - - $edges:{ e | - - $e.witness.clazz$ - $e.witness.line$ - 0 - $e.assumption$ - java::$e.witness.scope$ - }$ - - - \ No newline at end of file From 39ece250a5bbb571d1017cb35534351cd14cb457 Mon Sep 17 00:00:00 2001 From: Nils Loose Date: Thu, 15 Oct 2026 13:34:10 +0000 Subject: [PATCH 3/5] Add tests and SV-COMP task fixtures Unit tests for literals, escaping, markers, events, source resolution and columns; golden end-to-end tests on copies of SV-COMP tasks (the GDart example of sv-witnesses, jbmc-regression, jdart-regression, MinePump) and a synthetic task with an inner class, two nondet calls on one line, a packaged class and a unicode/control-character string. Generated witnesses are parsed with PyYAML / ElementTree, checked for exactly one final target segment and, when available, linted with WitnessLint. CLI tests cover the event file, both marker forms, the Java command line, exit codes and the zipapp. Co-Authored-By: Claude Opus 5.5 --- tests/conftest.py | 122 +++++++ .../MinePump/spec1-5_product25/Actions.java | 140 ++++++++ .../MinePump/spec1-5_product25/Main.java | 57 ++++ .../MinePumpSystem/Environment.java | 65 ++++ .../MinePumpSystem/MinePump.java | 92 +++++ tests/fixtures/README.md | 14 + .../org/sosy_lab/sv_benchmarks/Verifier.java | 101 ++++++ tests/fixtures/jbmc-regression/LICENSE | 39 +++ .../jbmc-regression/StringValueOf08/Main.java | 18 + .../jbmc-regression/exceptions16/Main.java | 32 ++ .../jbmc-regression/return2/Main.java | 17 + tests/fixtures/jdart-regression/LICENSE-MIT | 21 ++ .../fixtures/jdart-regression/float/Main.java | 16 + .../properties/no-runtime-exception.prp | 1 + .../no-runtime-exception.prp.license | 6 + tests/fixtures/properties/valid-assert.prp | 1 + .../properties/valid-assert.prp.license | 6 + .../ExDarko_false.witness-2.3.yml | 55 +++ .../sv-witnesses/LICENSE.Apache-2.0.txt | 208 ++++++++++++ .../org/sosy_lab/sv_benchmarks/Verifier.java | 92 +++++ .../jpf-regression/ExDarko_false/Main.java | 76 +++++ tests/fixtures/synthetic/Main.java | 31 ++ tests/fixtures/synthetic/pkg/Helper.java | 9 + tests/test_cli.py | 212 ++++++++++++ tests/test_e2e.py | 319 ++++++++++++++++++ tests/test_escaping.py | 102 ++++++ tests/test_events.py | 62 ++++ tests/test_literals.py | 141 ++++++++ tests/test_locations.py | 173 ++++++++++ tests/test_markers.py | 123 +++++++ 30 files changed, 2351 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/MinePump/spec1-5_product25/Actions.java create mode 100644 tests/fixtures/MinePump/spec1-5_product25/Main.java create mode 100644 tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/Environment.java create mode 100644 tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/MinePump.java create mode 100644 tests/fixtures/README.md create mode 100644 tests/fixtures/common/org/sosy_lab/sv_benchmarks/Verifier.java create mode 100644 tests/fixtures/jbmc-regression/LICENSE create mode 100644 tests/fixtures/jbmc-regression/StringValueOf08/Main.java create mode 100644 tests/fixtures/jbmc-regression/exceptions16/Main.java create mode 100644 tests/fixtures/jbmc-regression/return2/Main.java create mode 100644 tests/fixtures/jdart-regression/LICENSE-MIT create mode 100644 tests/fixtures/jdart-regression/float/Main.java create mode 100644 tests/fixtures/properties/no-runtime-exception.prp create mode 100644 tests/fixtures/properties/no-runtime-exception.prp.license create mode 100644 tests/fixtures/properties/valid-assert.prp create mode 100644 tests/fixtures/properties/valid-assert.prp.license create mode 100644 tests/fixtures/sv-witnesses/ExDarko_false.witness-2.3.yml create mode 100644 tests/fixtures/sv-witnesses/LICENSE.Apache-2.0.txt create mode 100644 tests/fixtures/sv-witnesses/java/common/org/sosy_lab/sv_benchmarks/Verifier.java create mode 100644 tests/fixtures/sv-witnesses/java/jpf-regression/ExDarko_false/Main.java create mode 100644 tests/fixtures/synthetic/Main.java create mode 100644 tests/fixtures/synthetic/pkg/Helper.java create mode 100644 tests/test_cli.py create mode 100644 tests/test_e2e.py create mode 100644 tests/test_escaping.py create mode 100644 tests/test_events.py create mode 100644 tests/test_literals.py create mode 100644 tests/test_locations.py create mode 100644 tests/test_markers.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ee04bc0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,122 @@ +import os +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +import pytest +import yaml + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +GRAPHML_NS = "{http://graphml.graphdrawing.org/xmlns}" + +# WitnessLint: a command prefix in $WITNESSLINT (e.g. "python3 /path/lint/witnesslinter.py"), +# or the developer's local installation. +_LOCAL_LINT = Path("/home/nils/dev/sv-comp-validators") +_LOCAL_LINT_CMD = [str(_LOCAL_LINT / "venv/bin/python"), str(_LOCAL_LINT / "witnesslint-2.3/lint/witnesslinter.py")] + + +def witnesslint_command(): + env = os.environ.get("WITNESSLINT") + if env: + return env.split() + if all(Path(p).exists() for p in _LOCAL_LINT_CMD): + return _LOCAL_LINT_CMD + return None + + +@pytest.fixture(scope="session") +def fixtures(): + return FIXTURES + + +@pytest.fixture +def witnesslint(): + """Runs WitnessLint on a YAML witness; skips the test when the linter is unavailable.""" + cmd = witnesslint_command() + if cmd is None: + pytest.skip("WitnessLint not available (set $WITNESSLINT)") + + def run(witness: Path, *programs: Path) -> str: + p = subprocess.run( + cmd + ["--expectViolationWitness", "--expectedWitnessVersion", "2", "--witness", str(witness)] + + [str(x) for x in programs], + capture_output=True, text=True, timeout=300, + ) + out = p.stdout + p.stderr + assert p.returncode == 0 and "finished with exit code 0" in out, out + return out + + return run + + +def load_yaml(path: Path): + """Parses the witness with PyYAML and checks the basic 2.3 structure.""" + doc = yaml.safe_load(path.read_text(encoding="utf-8")) + assert isinstance(doc, list) and len(doc) == 1 + entry = doc[0] + assert entry["entry_type"] == "violation_sequence" + md = entry["metadata"] + assert md["format_version"] == "2.3" + assert md["task"]["language"] == "Java" + assert "data_model" not in md["task"] + assert set(md["task"]["input_file_hashes"]) == set(md["task"]["input_files"]) + for seg in entry["content"]: + wps = seg["segment"] + assert len(wps) == 1 + wp = wps[0]["waypoint"] + assert wp["action"] == "follow" + assert wp["location"]["line"] >= 1 + assert wp["location"]["file_name"] in md["task"]["input_files"], wp + if "column" in wp["location"]: + assert wp["location"]["column"] >= 1 + if wp["type"] == "function_return": + assert wp["constraint"]["format"] == "java_expression" + else: + assert wp["type"] == "target" and "constraint" not in wp + return entry + + +def waypoints(entry): + return [seg["segment"][0]["waypoint"] for seg in entry["content"]] + + +def assert_single_final_target(entry): + """Exactly one target waypoint, and it is the last segment (WitnessLint does not check this).""" + wps = waypoints(entry) + targets = [i for i, wp in enumerate(wps) if wp["type"] == "target"] + assert targets == [len(wps) - 1], [wp["type"] for wp in wps] + + +def load_graphml(path: Path): + root = ET.parse(path).getroot() + graph = root.find(GRAPHML_NS + "graph") + data = {d.get("key"): d.text for d in graph.findall(GRAPHML_NS + "data")} + nodes = {n.get("id"): {d.get("key"): d.text for d in n.findall(GRAPHML_NS + "data")} for n in graph.findall(GRAPHML_NS + "node")} + edges = [] + for e in graph.findall(GRAPHML_NS + "edge"): + d = {x.get("key"): x.text for x in e.findall(GRAPHML_NS + "data")} + d["source"], d["target"] = e.get("source"), e.get("target") + edges.append(d) + return data, nodes, edges + + +def cli(*args, cwd=None): + """Runs the command line interface in a subprocess.""" + return subprocess.run( + [sys.executable, "-m", "witnesscreator", *map(str, args)], + capture_output=True, text=True, cwd=cwd, + ) + + +@pytest.fixture +def task_dir(tmp_path): + """A copy of a fixture task directory (so tests may write next to it).""" + + def copy(name: str) -> Path: + dst = tmp_path / Path(name).name + shutil.copytree(FIXTURES / name, dst) + return dst + + return copy diff --git a/tests/fixtures/MinePump/spec1-5_product25/Actions.java b/tests/fixtures/MinePump/spec1-5_product25/Actions.java new file mode 100644 index 0000000..0aa65e8 --- /dev/null +++ b/tests/fixtures/MinePump/spec1-5_product25/Actions.java @@ -0,0 +1,140 @@ +// This file is part of the SV-Benchmarks collection of verification tasks: +// https://gitlab.com/sosy-lab/benchmarking/sv-benchmarks +// +// SPDX-FileCopyrightText: 2011-2013 Alexander von Rhein, University of Passau +// SPDX-FileCopyrightText: 2011-2021 The SV-Benchmarks Community +// +// SPDX-License-Identifier: Apache-2.0 + +import MinePumpSystem.Environment; +import MinePumpSystem.MinePump; + +public class Actions { + + Environment env; + MinePump p; + + boolean methAndRunningLastTime = false; + boolean switchedOnBeforeTS = false; + + Actions() { + env = new Environment(); + p = new MinePump(env); + } + + void waterRise() { + env.waterRise(); + } + + void methaneChange() { + env.changeMethaneLevel(); + } + + void stopSystem() { + if (p.isSystemActive()) p.stopSystem(); + } + + void startSystem() { + if (!p.isSystemActive()) p.startSystem(); + } + + void timeShift() { + + if (p.isSystemActive()) Specification5_1(); + + p.timeShift(); + + if (p.isSystemActive()) { + Specification1(); + Specification2(); + Specification3(); + Specification4(); + Specification5_2(); + } + } + + String getSystemState() { + return p.toString(); + } + + // Specification 1 methan is Critical and pumping leads to Error + void Specification1() { + + Environment e = p.getEnv(); + + boolean b1 = e.isMethaneLevelCritical(); + boolean b2 = p.isPumpRunning(); + + if (b1 && b2) { + assert false; + } + } + + // Specification 2: When the pump is running, and there is methane, then it is + // in switched off at most 1 timesteps. + void Specification2() { + + Environment e = p.getEnv(); + + boolean b1 = e.isMethaneLevelCritical(); + boolean b2 = p.isPumpRunning(); + + if (b1 && b2) { + if (methAndRunningLastTime) { + assert false; + } else { + methAndRunningLastTime = true; + } + } else { + methAndRunningLastTime = false; + } + } + + // Specification 3: When the water is high and there is no methane, then the + // pump is on. + void Specification3() { + + Environment e = p.getEnv(); + + boolean b1 = e.isMethaneLevelCritical(); + boolean b2 = p.isPumpRunning(); + boolean b3 = e.getWaterLevel() == Environment.WaterLevelEnum.high; + + if (!b1 && b3 && !b2) { + assert false; + } + } + + // Specification 4: the pump is never on when the water level is low + void Specification4() { + + Environment e = p.getEnv(); + + boolean b2 = p.isPumpRunning(); + boolean b3 = e.getWaterLevel() == Environment.WaterLevelEnum.low; + + if (b3 && b2) { + assert false; + } + } + + // Specification 5: The Pump is never switched on when the water is below the + // highWater sensor. + void Specification5_1() { + switchedOnBeforeTS = p.isPumpRunning(); + } + + // Specification 5: The Pump is never switched on when the water is below the + // highWater sensor. + void Specification5_2() { + + Environment e = p.getEnv(); + + boolean b1 = p.isPumpRunning(); + boolean b2 = e.getWaterLevel() != Environment.WaterLevelEnum.high; + + if ((b2) && (b1 && !switchedOnBeforeTS)) { + assert false; + } + } +} diff --git a/tests/fixtures/MinePump/spec1-5_product25/Main.java b/tests/fixtures/MinePump/spec1-5_product25/Main.java new file mode 100644 index 0000000..7fce6cf --- /dev/null +++ b/tests/fixtures/MinePump/spec1-5_product25/Main.java @@ -0,0 +1,57 @@ +// This file is part of the SV-Benchmarks collection of verification tasks: +// https://gitlab.com/sosy-lab/benchmarking/sv-benchmarks +// +// SPDX-FileCopyrightText: 2011-2013 Alexander von Rhein, University of Passau +// SPDX-FileCopyrightText: 2011-2021 The SV-Benchmarks Community +// +// SPDX-License-Identifier: Apache-2.0 + +import org.sosy_lab.sv_benchmarks.Verifier; + +public class Main { + + private static int cleanupTimeShifts = 2; + + public static void main(String[] args) { + randomSequenceOfActions(3); + } + + public static boolean getBoolean() { + return Verifier.nondetBoolean(); + } + + public static void randomSequenceOfActions(int maxLength) { + Actions a = new Actions(); + + int counter = 0; + while (counter < maxLength) { + counter++; + + boolean action1 = getBoolean(); + boolean action2 = getBoolean(); + boolean action3 = getBoolean(); + boolean action4 = false; + if (!action3) action4 = getBoolean(); + + if (action1) { + a.waterRise(); + } + + if (action2) { + a.methaneChange(); + } + + if (action3) { + a.startSystem(); + } else if (action4) { + a.stopSystem(); + } + + a.timeShift(); + } + + for (counter = 0; counter < cleanupTimeShifts; counter++) { + a.timeShift(); + } + } +} diff --git a/tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/Environment.java b/tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/Environment.java new file mode 100644 index 0000000..ca33523 --- /dev/null +++ b/tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/Environment.java @@ -0,0 +1,65 @@ +// This file is part of the SV-Benchmarks collection of verification tasks: +// https://gitlab.com/sosy-lab/benchmarking/sv-benchmarks +// +// SPDX-FileCopyrightText: 2011-2013 Alexander von Rhein, University of Passau +// SPDX-FileCopyrightText: 2011-2021 The SV-Benchmarks Community +// +// SPDX-License-Identifier: Apache-2.0 + +package MinePumpSystem; + +public class Environment { + + public enum WaterLevelEnum { + low, + normal, + high + } + + private WaterLevelEnum waterLevel = WaterLevelEnum.normal; + + private boolean methaneLevelCritical = false; + + void lowerWaterLevel() { + switch (waterLevel) { + case high: + waterLevel = WaterLevelEnum.normal; + break; + case normal: + waterLevel = WaterLevelEnum.low; + break; + } + } + + public void waterRise() { + switch (waterLevel) { + case low: + waterLevel = WaterLevelEnum.normal; + break; + case normal: + waterLevel = WaterLevelEnum.high; + break; + } + } + + public void changeMethaneLevel() { + methaneLevelCritical = !methaneLevelCritical; + } + + public boolean isMethaneLevelCritical() { + return methaneLevelCritical; + } + + @Override + public String toString() { + return "Env(Water:" + waterLevel + ",Meth:" + (methaneLevelCritical ? "CRIT" : "OK") + ")"; + } + + public WaterLevelEnum getWaterLevel() { + return waterLevel; + } + + boolean isLowWaterSensorDry() { + return waterLevel == WaterLevelEnum.low; + } +} diff --git a/tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/MinePump.java b/tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/MinePump.java new file mode 100644 index 0000000..087f984 --- /dev/null +++ b/tests/fixtures/MinePump/spec1-5_product25/MinePumpSystem/MinePump.java @@ -0,0 +1,92 @@ +// This file is part of the SV-Benchmarks collection of verification tasks: +// https://gitlab.com/sosy-lab/benchmarking/sv-benchmarks +// +// SPDX-FileCopyrightText: 2011-2013 Alexander von Rhein, University of Passau +// SPDX-FileCopyrightText: 2011-2021 The SV-Benchmarks Community +// +// SPDX-License-Identifier: Apache-2.0 + +package MinePumpSystem; + +public class MinePump { + + boolean pumpRunning = false; + + boolean systemActive = true; + + Environment env; + + public MinePump(Environment env) { + super(); + this.env = env; + } + + public void timeShift() { + if (pumpRunning) env.lowerWaterLevel(); + if (systemActive) processEnvironment(); + } + + private void processEnvironment__wrappee__base() {} + + public void processEnvironment() { + if (pumpRunning && isLowWaterLevel()) { + deactivatePump(); + } else { + processEnvironment__wrappee__base(); + } + } + + private void activatePump__wrappee__lowWaterSensor() { + pumpRunning = true; + } + + void activatePump() { + if (!isMethaneAlarm()) { + activatePump__wrappee__lowWaterSensor(); + } else { + // System.out.println("Pump not activated due to methane alarm"); + } + } + + public boolean isPumpRunning() { + return pumpRunning; + } + + void deactivatePump() { + pumpRunning = false; + } + + boolean isMethaneAlarm() { + return env.isMethaneLevelCritical(); + } + + @Override + public String toString() { + return "Pump(System:" + + (systemActive ? "On" : "Off") + + ",Pump:" + + (pumpRunning ? "On" : "Off") + + ") " + + env.toString(); + } + + public Environment getEnv() { + return env; + } + + boolean isLowWaterLevel() { + return !env.isLowWaterSensorDry(); + } + + public void stopSystem() { + // feature not present + } + + public void startSystem() { + // feature not present + } + + public boolean isSystemActive() { + return systemActive; + } +} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..915ac08 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,14 @@ +# Test fixtures + +Small verification tasks used by the golden tests. They are copies of files from +the [SV-Benchmarks](https://gitlab.com/sosy-lab/benchmarking/sv-benchmarks) and +[sv-witnesses](https://gitlab.com/sosy-lab/benchmarking/sv-witnesses) repositories +and keep their original license headers: + +| Directory | Origin | License | +|---|---|---| +| `common/`, `properties/`, `MinePump/` | sv-benchmarks `java/` | Apache-2.0 (SPDX headers in the files) | +| `jbmc-regression/` | sv-benchmarks `java/jbmc-regression` | 4-clause BSD, see `jbmc-regression/LICENSE` | +| `jdart-regression/` | sv-benchmarks `java/jdart-regression` | MIT, see `jdart-regression/LICENSE-MIT` | +| `sv-witnesses/` | sv-witnesses `examples/java` (the GDart example witness and its task) | Apache-2.0, see `sv-witnesses/LICENSE.Apache-2.0.txt` | +| `synthetic/` | written for these tests | same license as this repository | diff --git a/tests/fixtures/common/org/sosy_lab/sv_benchmarks/Verifier.java b/tests/fixtures/common/org/sosy_lab/sv_benchmarks/Verifier.java new file mode 100644 index 0000000..8769f84 --- /dev/null +++ b/tests/fixtures/common/org/sosy_lab/sv_benchmarks/Verifier.java @@ -0,0 +1,101 @@ +// This file is part of the SV-Benchmarks collection of verification tasks: +// https://github.com/sosy-lab/sv-benchmarks +// +// SPDX-FileCopyrightText: Contributed by Peter Schrammel +// SPDX-FileCopyrightText: 2011-2025 The SV-Benchmarks Community +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.sv_benchmarks; + +import java.util.Random; + +/** + * This class provides an interface between the benchmarks and the verification tools for + * + *
    + *
  • defining assumptions + *
  • generating nondeterministic values + *
+ * + * The verification semantics of the methods is defined in the method documentations below. The + * bodies of the methods are only approximate to allow execution, but do NOT define the verification + * semantics. + * + *

Also see java/README.md + */ +public final class Verifier { + /** + * Assume that the given condition holds true. Any execution that does not satisfy the condition + * cannot proceed. + * + * @param condition The assumption + */ + public static void assume(boolean condition) { + if (!condition) { + Runtime.getRuntime().halt(1); + } + } + + /** @return A nondeterministically chosen Boolean value (true or false) */ + public static boolean nondetBoolean() { + return new Random().nextBoolean(); + } + + /** @return A nondeterministicly chosen 8-bit signed integer value */ + public static byte nondetByte() { + return (byte) (new Random().nextInt()); + } + + /** @return A nondeterministicly chosen 16-bit unsigned integer value */ + public static char nondetChar() { + return (char) (new Random().nextInt()); + } + + /** @return A nondeterministicly chosen 16-bit signed integer value */ + public static short nondetShort() { + return (short) (new Random().nextInt()); + } + + /** @return A nondeterministicly chosen 32-bit signed integer value */ + public static int nondetInt() { + return new Random().nextInt(); + } + + /** @return A nondeterministicly chosen 64-bit signed integer value */ + public static long nondetLong() { + return new Random().nextLong(); + } + + /** @return A nondeterministicly chosen 32-bit IEEE 754 single-precision floating point value */ + public static float nondetFloat() { + return new Random().nextFloat(); + } + + /** @return A nondeterministicly chosen 64-bit IEEE 754 double-precision floating point value */ + public static double nondetDouble() { + return new Random().nextDouble(); + } + + /** + * @return A non-null character string of nondeterministically chosen length (in range + * [0,2147483647]) and consisting of nondeterministically chosen characters. + */ + public static String nondetString() { + Random random = new Random(); + int size = random.nextInt(); + assume(size >= 0); + byte[] bytes = new byte[size]; + random.nextBytes(bytes); + return new String(bytes); + } + + /** + * @return An object of given type chosen by the given factory implementation. + * @implnote For nondeterministic choice, the factory implementation must use the nondet methods + * above. + */ + public static T nondetObject(Class type, ObjectFactory factory) { + return factory.createObject(); + } +} diff --git a/tests/fixtures/jbmc-regression/LICENSE b/tests/fixtures/jbmc-regression/LICENSE new file mode 100644 index 0000000..50673cf --- /dev/null +++ b/tests/fixtures/jbmc-regression/LICENSE @@ -0,0 +1,39 @@ +(C) 2001-2016, Daniel Kroening, Edmund Clarke, +Computer Science Department, University of Oxford +Computer Science Department, Carnegie Mellon University + +All rights reserved. Redistribution and use in source and binary forms, with +or without modification, are permitted provided that the following +conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + + This product includes software developed by Daniel Kroening, + Edmund Clarke, + Computer Science Department, University of Oxford + Computer Science Department, Carnegie Mellon University + + 4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/jbmc-regression/StringValueOf08/Main.java b/tests/fixtures/jbmc-regression/StringValueOf08/Main.java new file mode 100644 index 0000000..3bb0344 --- /dev/null +++ b/tests/fixtures/jbmc-regression/StringValueOf08/Main.java @@ -0,0 +1,18 @@ +/* + * Origin of the benchmark: + * license: 4-clause BSD (see /java/jbmc-regression/LICENSE) + * repo: https://github.com/diffblue/cbmc.git + * branch: develop + * directory: regression/jbmc-strings/StringValueOf08 + * The benchmark was taken from the repo: 24 January 2018 + */ +import org.sosy_lab.sv_benchmarks.Verifier; + +public class Main { + public static void main(String[] args) { + String arg = Verifier.nondetString(); + float floatValue = Float.parseFloat(arg); + String tmp = String.valueOf(floatValue); + assert tmp.equals("2.50"); + } +} diff --git a/tests/fixtures/jbmc-regression/exceptions16/Main.java b/tests/fixtures/jbmc-regression/exceptions16/Main.java new file mode 100644 index 0000000..87aa147 --- /dev/null +++ b/tests/fixtures/jbmc-regression/exceptions16/Main.java @@ -0,0 +1,32 @@ +/* + * Origin of the benchmark: + * license: 4-clause BSD (see /java/jbmc-regression/LICENSE) + * repo: https://github.com/diffblue/cbmc.git + * branch: develop + * directory: regression/cbmc-java/exceptions16 + * The benchmark was taken from the repo: 24 January 2018 + */ +import org.sosy_lab.sv_benchmarks.Verifier; + +class A extends RuntimeException {} + +class B extends A {} + +class C extends B {} + +public class Main { + public static void main(String[] args) { + int x = Verifier.nondetInt(); + try { + x++; + } catch (A exc) { + assert false; + } + + try { + throw new B(); + } catch (B exc) { + assert false; + } + } +} diff --git a/tests/fixtures/jbmc-regression/return2/Main.java b/tests/fixtures/jbmc-regression/return2/Main.java new file mode 100644 index 0000000..e6c7a8b --- /dev/null +++ b/tests/fixtures/jbmc-regression/return2/Main.java @@ -0,0 +1,17 @@ +/* + * Origin of the benchmark: + * license: 4-clause BSD (see /java/jbmc-regression/LICENSE) + * repo: https://github.com/diffblue/cbmc.git + * branch: develop + * directory: regression/cbmc-java/return2 + * The benchmark was taken from the repo: 24 January 2018 + */ +import org.sosy_lab.sv_benchmarks.Verifier; + +class Main { + public static void main(String[] args) { + int v1 = Verifier.nondetInt(); + int v2 = Verifier.nondetInt(); + assert v1 == v2; // should be able to fail + } +} diff --git a/tests/fixtures/jdart-regression/LICENSE-MIT b/tests/fixtures/jdart-regression/LICENSE-MIT new file mode 100644 index 0000000..8c2aebe --- /dev/null +++ b/tests/fixtures/jdart-regression/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 TU Dortmund, Falk Howar and Malte Mues + +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. diff --git a/tests/fixtures/jdart-regression/float/Main.java b/tests/fixtures/jdart-regression/float/Main.java new file mode 100644 index 0000000..80783f3 --- /dev/null +++ b/tests/fixtures/jdart-regression/float/Main.java @@ -0,0 +1,16 @@ +/* + * Contributed to SV-COMP by Falk Howar + * License: MIT (see /java/jdart-regression/LICENSE-MIT) + * + */ + +import org.sosy_lab.sv_benchmarks.Verifier; + +public class Main { + + public static void main(String[] args) { + float a = Verifier.nondetFloat(); + float b = Verifier.nondetFloat(); + assert (a + b != a || b == 0.0); + } +} diff --git a/tests/fixtures/properties/no-runtime-exception.prp b/tests/fixtures/properties/no-runtime-exception.prp new file mode 100644 index 0000000..55b5c59 --- /dev/null +++ b/tests/fixtures/properties/no-runtime-exception.prp @@ -0,0 +1 @@ +CHECK(init(Main.main()), LTL(G ! uncaught(java.lang.RuntimeException))) diff --git a/tests/fixtures/properties/no-runtime-exception.prp.license b/tests/fixtures/properties/no-runtime-exception.prp.license new file mode 100644 index 0000000..b76c19f --- /dev/null +++ b/tests/fixtures/properties/no-runtime-exception.prp.license @@ -0,0 +1,6 @@ +This file is part of the SV-Benchmarks collection of verification tasks: +https://github.com/sosy-lab/sv-benchmarks + +SPDX-FileCopyrightText: 2011-2020 The SV-Benchmarks Community + +SPDX-License-Identifier: Apache-2.0 diff --git a/tests/fixtures/properties/valid-assert.prp b/tests/fixtures/properties/valid-assert.prp new file mode 100644 index 0000000..a9d5a52 --- /dev/null +++ b/tests/fixtures/properties/valid-assert.prp @@ -0,0 +1 @@ +CHECK( init(Main.main()), LTL(G assert) ) diff --git a/tests/fixtures/properties/valid-assert.prp.license b/tests/fixtures/properties/valid-assert.prp.license new file mode 100644 index 0000000..5a43e6b --- /dev/null +++ b/tests/fixtures/properties/valid-assert.prp.license @@ -0,0 +1,6 @@ +This file is part of the SV-Benchmarks collection of verification tasks: +https://github.com/sosy-lab/sv-benchmarks + +SPDX-FileCopyrightText: 2011-2020 The SV-Benchmarks Community + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tests/fixtures/sv-witnesses/ExDarko_false.witness-2.3.yml b/tests/fixtures/sv-witnesses/ExDarko_false.witness-2.3.yml new file mode 100644 index 0000000..3633213 --- /dev/null +++ b/tests/fixtures/sv-witnesses/ExDarko_false.witness-2.3.yml @@ -0,0 +1,55 @@ +# This file is part of the SV-Witnesses repository as regression test: +# https://gitlab.com/sosy-lab/benchmarking/sv-witnesses +# +# SPDX-FileCopyrightText: 2026 The SV-Witnesses Community +# +# SPDX-License-Identifier: Apache-2.0 +--- +- entry_type: "violation_sequence" + metadata: + creation_time: "2026-09-08T17:36:11Z" + format_version: "2.3" + producer: + name: "GDart" + version: "VERSION_PLACEHOLDER" + task: + input_file_hashes: + ./java/common/org/sosy_lab/sv_benchmarks/Verifier.java: "d544c9e74f86b4ed8e692b7b7bcb00ec3901e19f5b0d4740b49e5c3c672031ba" + ./java/jpf-regression/ExDarko_false/Main.java: "44faed0b4c09127009615aa7c0699fb1acda5235fdcd1bbcbea867661d450d82" + input_files: + - "./java/common/org/sosy_lab/sv_benchmarks/Verifier.java" + - "./java/jpf-regression/ExDarko_false/Main.java" + language: "Java" + specification: "CHECK( init(Main.main()), LTL(G assert) )" + uuid: "be742527-8e82-4418-9ba2-6cbb857e25a7" + content: + - segment: + - waypoint: + type: "function_return" + action: "follow" + constraint: + value: "\\result == 0" + format: "java_expression" + location: + file_name: "./java/jpf-regression/ExDarko_false/Main.java" + line: 34 + column: 43 + - segment: + - waypoint: + type: "function_return" + action: "follow" + constraint: + value: "\\result == 0" + format: "java_expression" + location: + file_name: "./java/jpf-regression/ExDarko_false/Main.java" + line: 35 + column: 41 + - segment: + - waypoint: + type: "target" + action: "follow" + location: + file_name: "./java/jpf-regression/ExDarko_false/Main.java" + line: 48 + column: 12 diff --git a/tests/fixtures/sv-witnesses/LICENSE.Apache-2.0.txt b/tests/fixtures/sv-witnesses/LICENSE.Apache-2.0.txt new file mode 100644 index 0000000..4ed90b9 --- /dev/null +++ b/tests/fixtures/sv-witnesses/LICENSE.Apache-2.0.txt @@ -0,0 +1,208 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, +AND DISTRIBUTION + + 1. Definitions. + + + +"License" shall mean the terms and conditions for use, reproduction, and distribution +as defined by Sections 1 through 9 of this document. + + + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + + + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct +or indirect, to cause the direction or management of such entity, whether +by contract or otherwise, or (ii) ownership of fifty percent (50%) or more +of the outstanding shares, or (iii) beneficial ownership of such entity. + + + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions +granted by this License. + + + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + + + +"Object" form shall mean any form resulting from mechanical transformation +or translation of a Source form, including but not limited to compiled object +code, generated documentation, and conversions to other media types. + + + +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that +is included in or attached to the work (an example is provided in the Appendix +below). + + + +"Derivative Works" shall mean any work, whether in Source or Object form, +that is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative +Works shall not include works that remain separable from, or merely link (or +bind by name) to the interfaces of, the Work and Derivative Works thereof. + + + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative +Works thereof, that is intentionally submitted to Licensor for inclusion in +the Work by the copyright owner or by an individual or Legal Entity authorized +to submit on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication +sent to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor +for the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + + + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently incorporated +within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute +the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their Contribution(s) +alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against +any entity (including a cross-claim or counterclaim in a lawsuit) alleging +that the Work or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses granted to You +under this License for that Work shall terminate as of the date such litigation +is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and +in Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy +of this License; and + +(b) You must cause any modified files to carry prominent notices stating that +You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source +form of the Work, excluding those notices that do not pertain to any part +of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, +then any Derivative Works that You distribute must include a readable copy +of the attribution notices contained within such NOTICE file, excluding those +notices that do not pertain to any part of the Derivative Works, in at least +one of the following places: within a NOTICE text file distributed as part +of the Derivative Works; within the Source form or documentation, if provided +along with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works +that You distribute, alongside or as an addendum to the NOTICE text from the +Work, provided that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, +or distribution of Your modifications, or for any such Derivative Works as +a whole, provided Your use, reproduction, and distribution of the Work otherwise +complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without +any additional terms or conditions. Notwithstanding the above, nothing herein +shall supersede or modify the terms of any separate license agreement you +may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to +in writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR +A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness +of using or redistributing the Work and assume any risks associated with Your +exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether +in tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to +in writing, shall any Contributor be liable to You for damages, including +any direct, indirect, special, incidental, or consequential damages of any +character arising as a result of this License or out of the use or inability +to use the Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all other commercial +damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work +or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such obligations, +You may act only on Your own behalf and on Your sole responsibility, not on +behalf of any other Contributor, and only if You agree to indemnify, defend, +and hold each Contributor harmless for any liability incurred by, or claims +asserted against, such Contributor by reason of your accepting any such warranty +or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own identifying +information. (Don't include the brackets!) The text should be enclosed in +the appropriate comment syntax for the file format. We also recommend that +a file or class name and description of purpose be included on the same "printed +page" as the copyright notice for easier identification within third-party +archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. diff --git a/tests/fixtures/sv-witnesses/java/common/org/sosy_lab/sv_benchmarks/Verifier.java b/tests/fixtures/sv-witnesses/java/common/org/sosy_lab/sv_benchmarks/Verifier.java new file mode 100644 index 0000000..e0ff042 --- /dev/null +++ b/tests/fixtures/sv-witnesses/java/common/org/sosy_lab/sv_benchmarks/Verifier.java @@ -0,0 +1,92 @@ +// This file is part of the SV-Benchmarks collection of verification tasks: +// https://github.com/sosy-lab/sv-benchmarks +// +// SPDX-FileCopyrightText: Contributed by Peter Schrammel +// SPDX-FileCopyrightText: 2011-2025 The SV-Benchmarks Community +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.sv_benchmarks; + +import java.util.Random; + +/** + * This class provides an interface between the benchmarks and the verification tools for + * + *

    + *
  • defining assumptions + *
  • generating nondeterministic values + *
+ * + * The verification semantics of the methods is defined in the method documentations below. The + * bodies of the methods are only approximate to allow execution, but do NOT define the verification + * semantics. + * + *

Also see java/README.md + */ +public final class Verifier { + /** + * Assume that the given condition holds true. Any execution that does not satisfy the condition + * cannot proceed. + * + * @param condition The assumption + */ + public static void assume(boolean condition) { + if (!condition) { + Runtime.getRuntime().halt(1); + } + } + + /** @return A nondeterministically chosen Boolean value (true or false) */ + public static boolean nondetBoolean() { + return new Random().nextBoolean(); + } + + /** @return A nondeterministicly chosen 8-bit signed integer value */ + public static byte nondetByte() { + return (byte) (new Random().nextInt()); + } + + /** @return A nondeterministicly chosen 16-bit unsigned integer value */ + public static char nondetChar() { + return (char) (new Random().nextInt()); + } + + /** @return A nondeterministicly chosen 16-bit signed integer value */ + public static short nondetShort() { + return (short) (new Random().nextInt()); + } + + /** @return A nondeterministicly chosen 32-bit signed integer value */ + public static int nondetInt() { + return new Random().nextInt(); + } + + /** @return A nondeterministicly chosen 64-bit signed integer value */ + public static long nondetLong() { + return new Random().nextLong(); + } + + /** @return A nondeterministicly chosen 32-bit IEEE 754 single-precision floating point value */ + public static float nondetFloat() { + return new Random().nextFloat(); + } + + /** @return A nondeterministicly chosen 64-bit IEEE 754 double-precision floating point value */ + public static double nondetDouble() { + return new Random().nextDouble(); + } + + /** + * @return A non-null character string of nondeterministically chosen length (in range + * [0,2147483647]) and consisting of nondeterministically chosen characters. + */ + public static String nondetString() { + Random random = new Random(); + int size = random.nextInt(); + assume(size >= 0); + byte[] bytes = new byte[size]; + random.nextBytes(bytes); + return new String(bytes); + } +} diff --git a/tests/fixtures/sv-witnesses/java/jpf-regression/ExDarko_false/Main.java b/tests/fixtures/sv-witnesses/java/jpf-regression/ExDarko_false/Main.java new file mode 100644 index 0000000..d07247a --- /dev/null +++ b/tests/fixtures/sv-witnesses/java/jpf-regression/ExDarko_false/Main.java @@ -0,0 +1,76 @@ +/* + * Origin of the benchmark: + * repo: https://babelfish.arc.nasa.gov/hg/jpf/jpf-symbc + * branch: updated-spf + * root directory: src/tests/gov/nasa/jpf/symbc + * The benchmark was taken from the repo: 24 January 2018 + */ +/* + * Copyright (C) 2014, United States Government, as represented by the + * Administrator of the National Aeronautics and Space Administration. + * All rights reserved. + * + * Symbolic Pathfinder (jpf-symbc) is licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * SPDX-FileCopyrightText: Copyright (C) 2014, United States Government, as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// package gov.nasa.jpf.symbc; +import org.sosy_lab.sv_benchmarks.Verifier; + +public class Main { + + public static void main(String[] args) { + new Main().unboxed(Verifier.nondetInt(), 2); + new Main().boxed(Verifier.nondetInt(), 2); + new Main().customBoxed(Verifier.nondetInt(), 2); + } + + private void unboxed(int i, int j) { + if (i == j) { + System.out.println("HIT"); + } + } + + private void boxed(int i, int j) { + if (new Integer(i).equals(new Integer(j))) { + System.out.println("HIT"); + } else assert false; + } + + private void customBoxed(int i, int j) { + if (new MyInteger(i).equals(new MyInteger(j))) { + System.out.println("HIT"); + } + } +} + +class MyInteger { + int value; + + public MyInteger(int value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MyInteger)) { + return false; + } + MyInteger other = (MyInteger) obj; + return this.value == other.value; + } +} diff --git a/tests/fixtures/synthetic/Main.java b/tests/fixtures/synthetic/Main.java new file mode 100644 index 0000000..319d5e2 --- /dev/null +++ b/tests/fixtures/synthetic/Main.java @@ -0,0 +1,31 @@ +import org.sosy_lab.sv_benchmarks.Verifier; +import pkg.Helper; + +public class Main { + static class Inner { + int pick() { + // Verifier.nondetInt() in a comment + String s = "Verifier.nondetInt()"; // and in a string literal + return Verifier.nondetInt() + Verifier.nondetInt(); // two calls, one line + } + } + + public static void main(String[] args) { + char c = Verifier.nondetChar(); + double d = Verifier.nondetDouble(); + float f = Verifier.nondetFloat(); + long l = Verifier.nondetLong(); + String s = Verifier.nondetString(); + int sum = new Inner().pick(); + if (Helper.check(sum) && Companion.flag() && s.length() > 0) { + throw new IllegalStateException("boom"); + } + assert false; + } +} + +class Companion { + static boolean flag() { + return Verifier.nondetBoolean(); + } +} diff --git a/tests/fixtures/synthetic/pkg/Helper.java b/tests/fixtures/synthetic/pkg/Helper.java new file mode 100644 index 0000000..42d0216 --- /dev/null +++ b/tests/fixtures/synthetic/pkg/Helper.java @@ -0,0 +1,9 @@ +package pkg; + +import org.sosy_lab.sv_benchmarks.Verifier; + +public class Helper { + public static boolean check(int x) { + return x > Verifier.nondetInt(); + } +} diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..344d0b0 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,212 @@ +import base64 +import shutil + +import yaml + +from conftest import cli, load_graphml, load_yaml, waypoints +from witnesscreator import Frame, Location, NondetReturn, Violation, dump_events +from witnesscreator.cli import main + + +def b64(s: str) -> str: + return base64.b64encode(s.encode()).decode() + + +def marker(kind: str, payload: str) -> str: + return f"prefix {kind} {b64(payload)}" + + +def write_task(dir_): + dir_.mkdir(parents=True, exist_ok=True) + (dir_ / "Main.java").write_text( + "import org.sosy_lab.sv_benchmarks.Verifier;\n" + "public class Main {\n" + " public static void main(String[] args) {\n" + " int x = Verifier.nondetInt();\n" + " String s = Verifier.nondetString();\n" + " if (x == 3 && s.equals(\"a\\\"b\")) {\n" + " assert false;\n" + " }\n" + " }\n" + "}\n" + ) + prp = dir_.parent / "valid-assert.prp" + prp.write_text("CHECK( init(Main.main()), LTL(G assert) )\n") + return prp + + +MARKERS = "\n".join([ + marker("[WITNESS]", "3@@@4@@@Main@@@main([Ljava/lang/String;)V@@@0@@@I@@@Main.java"), + marker("[WITNESS]", "a\"b@@@5@@@Main@@@main([Ljava/lang/String;)V@@@0@@@Ljava/lang/String;@@@Main.java"), + marker("[WITNESS_TARGET]", "java.lang.AssertionError@@@Main@@Main.java@@7"), +]) + + +def check_witness(out, task): + entry = load_yaml(out / "witness.yml") + wps = waypoints(entry) + assert [wp["type"] for wp in wps] == ["function_return", "function_return", "target"] + assert wps[0]["constraint"]["value"] == "\\result == 3" + assert wps[0]["location"] == {"file_name": f"{task}/Main.java", "line": 4, "column": 32} + assert wps[1]["constraint"]["value"] == '\\result.equals("a\\"b")' + assert wps[1]["location"]["column"] == 38 + assert wps[2]["location"] == {"file_name": f"{task}/Main.java", "line": 7, "column": 7} + _, nodes, edges = load_graphml(out / "witness.graphml") + assert [e["assumption"] for e in edges] == ["\\result = 3;", '\\result.equals("a\\"b");'] + assert edges[0]["assumption.scope"] == "java::LMain;.main([Ljava/lang/String;)V" + assert edges[0]["originfile"] == "Main.java" + assert nodes["n2"] == {"violation": "true"} + + +def test_java_compatible_command_line(tmp_path): + task = tmp_path / "task" + prp = write_task(task) + out = tmp_path / "out" + p = cli(b64(MARKERS), "--input", task, "--property", prp, "--out", out, "--producer-version", "test") + assert p.returncode == 0, p.stderr + entry = load_yaml(out / "witness.yml") + assert entry["metadata"]["producer"] == {"name": "SWAT", "version": "test"} + assert entry["metadata"]["task"]["specification"] == "CHECK( init(Main.main()), LTL(G assert) )" + check_witness(out, task) + assert "Writing violation witness" in p.stderr + + +def test_legacy_positional_dir(tmp_path): + task = tmp_path / "task" + write_task(task) + assert main([b64(MARKERS), str(task)]) == 0 + check_witness(task, task) + entry = load_yaml(task / "witness.yml") + assert entry["metadata"]["producer"] == {"name": "SWAT", "version": "unknown"} + assert entry["metadata"]["task"]["specification"] == "CHECK( init(Main.main()), LTL(G assert) )" + + +def test_legacy_subcommand_with_markers_file(tmp_path): + task = tmp_path / "task" + prp = write_task(task) + markers = tmp_path / "markers.txt" + markers.write_text(MARKERS + "\n") + out = tmp_path / "out" + assert main(["legacy", "--markers-file", str(markers), "--input", str(task), "--property", str(prp), + "--out-dir", str(out), "--producer-name", "Other", "--producer-version", "1"]) == 0 + check_witness(out, task) + assert load_yaml(out / "witness.yml")["metadata"]["producer"] == {"name": "Other", "version": "1"} + # the file may hold the base64 blob as well + markers.write_text(b64(MARKERS)) + out2 = tmp_path / "out2" + assert main(["legacy", "--markers-file", str(markers), "--input", str(task), "--out-dir", str(out2), "--format", "yaml"]) == 0 + assert (out2 / "witness.yml").exists() and not (out2 / "witness.graphml").exists() + + +def test_legacy_empty_markers_write_default_violation(tmp_path): + task = tmp_path / "task" + write_task(task) + out = tmp_path / "out" + p = cli("", "--input", task, "--out", out) + assert p.returncode == 0, p.stderr + entry = load_yaml(out / "witness.yml") + assert entry["content"] == [] + _, nodes, edges = load_graphml(out / "witness.graphml") + assert nodes["n0"] == {"entry": "true", "violation": "true"} and edges == [] + # a target alone yields a single final target segment + p = cli(b64(marker("[WITNESS_TARGET]", "java.lang.AssertionError@@@Main@@Main.java@@7")), "--input", task, "--out", out) + assert p.returncode == 0 + wps = waypoints(load_yaml(out / "witness.yml")) + assert len(wps) == 1 and wps[0]["type"] == "target" and wps[0]["location"]["column"] == 7 + + +def test_legacy_four_field_markers(tmp_path): + (tmp_path / "Main.java").write_text("class Main {\n int x = Verifier.nondetInt();\n}\n") + markers = marker("[WITNESS]", "1@@@2@@@Main@@@()V") + assert main([b64(markers), str(tmp_path)]) == 0 + wps = waypoints(load_yaml(tmp_path / "witness.yml")) + assert wps[0]["constraint"]["value"] == "\\result == 1" + assert wps[0]["location"]["line"] == 2 and wps[0]["location"]["column"] == 30 + text = (tmp_path / "witness.graphml").read_text() + assert "<init>" in text + _, _, edges = load_graphml(tmp_path / "witness.graphml") + assert edges[0]["assumption.scope"] == "java::LMain;.()V" + + +def test_events_command(tmp_path): + task = tmp_path / "task" + prp = write_task(task) + values = [ + NondetReturn("nondetInt", "int", "3", Location(class_name="Main", source_file="Main.java", line=4, call_index=0, method="main([Ljava/lang/String;)V")), + NondetReturn("nondetString", "String", 'a"b', Location(file_name=f"{task}/Main.java", line=5, column=38)), + ] + violation = Violation("java.lang.AssertionError", [Frame("Main", "Main.java", 7)]) + events = tmp_path / "events.jsonl" + events.write_text(dump_events(values, violation, "SWAT", "svcomp27")) + out = tmp_path / "out" + p = cli("events", "--events", events, "--property", prp, "--input", task, "--out-dir", out) + assert p.returncode == 0, p.stderr + entry = load_yaml(out / "witness.yml") + assert entry["metadata"]["producer"] == {"name": "SWAT", "version": "svcomp27"} + wps = waypoints(entry) + assert wps[0]["location"]["column"] == 32 and wps[1]["location"]["column"] == 38 + assert wps[2]["type"] == "target" + _, _, edges = load_graphml(out / "witness.graphml") + assert edges[0]["assumption.scope"] == "java::LMain;.main([Ljava/lang/String;)V" + assert edges[1]["assumption.scope"] == "java::LMain;" + # command line overrides the header + p = cli("events", "--events", events, "--property", prp, "--input", task, "--out-dir", out, + "--producer-name", "X", "--producer-version", "Y", "--format", "graphml") + assert p.returncode == 0 + data, _, _ = load_graphml(out / "witness.graphml") + assert data["producer"] == "X" + + +def test_events_missing_file_still_writes(tmp_path): + task = tmp_path / "task" + write_task(task) + out = tmp_path / "out" + p = cli("events", "--events", tmp_path / "nope.jsonl", "--input", task, "--out-dir", out) + assert p.returncode == 0 + assert "Could not read event file" in p.stderr + assert load_yaml(out / "witness.yml")["content"] == [] + assert "No property file given" in p.stderr + + +def test_usage_errors(tmp_path): + assert cli().returncode == 2 + assert cli("events").returncode == 2 + assert cli("legacy", "--format", "xml").returncode == 2 + p = cli("--version") + assert p.returncode == 0 and "witnesscreator" in p.stdout + assert cli("--help").returncode == 0 + assert cli("legacy", "--help").returncode == 0 + + +def test_nothing_written_is_exit_1(tmp_path): + blocker = tmp_path / "file" + blocker.write_text("not a directory") + p = cli("", "--out", blocker / "out") + assert p.returncode == 1 + assert "Could not" in p.stderr + + +def test_quiet_and_verbose(tmp_path): + task = tmp_path / "task" + write_task(task) + p = cli("-q", "legacy", b64(MARKERS), str(task)) + assert p.returncode == 0 and "INFO" not in p.stderr + p = cli("-v", "legacy", b64(MARKERS), str(task)) + assert p.returncode == 0 and "INFO" in p.stderr + + +def test_pyz_build_and_run(tmp_path): + import subprocess + import sys + from pathlib import Path + + tool = Path(__file__).resolve().parent.parent / "tools" / "build_pyz.py" + pyz = tmp_path / "witnesscreator.pyz" + assert subprocess.run([sys.executable, str(tool), str(pyz)], capture_output=True).returncode == 0 + task = tmp_path / "task" + write_task(task) + p = subprocess.run([sys.executable, str(pyz), b64(MARKERS), str(task)], capture_output=True, text=True) + assert p.returncode == 0, p.stderr + check_witness(task, task) + p = subprocess.run([sys.executable, str(pyz), "--version"], capture_output=True, text=True) + assert p.stdout.startswith("witnesscreator ") diff --git a/tests/test_e2e.py b/tests/test_e2e.py new file mode 100644 index 0000000..eb023c8 --- /dev/null +++ b/tests/test_e2e.py @@ -0,0 +1,319 @@ +"""Golden end-to-end tests on real SV-COMP tasks (see fixtures/README.md).""" + +import logging + +import pytest +import yaml + +from conftest import FIXTURES, assert_single_final_target, load_graphml, load_yaml, waypoints +from witnesscreator import Frame, Location, NondetReturn, Task, Violation, parse_markers, write_witnesses +from witnesscreator import witness as witness_module + +COMMON = FIXTURES / "common" +PROPS = FIXTURES / "properties" +VALID_ASSERT = PROPS / "valid-assert.prp" +NO_RUNTIME_EXCEPTION = PROPS / "no-runtime-exception.prp" + + +def task_for(task_dir, prp=VALID_ASSERT, **kw): + return Task(input_files=[str(COMMON), str(task_dir)], property_file=str(prp), + producer_name="test", producer_version="0", **kw) + + +def write(task, values, violation, out, **kw): + paths = write_witnesses(task, values, violation, out, **kw) + assert set(paths) == {"yaml", "graphml"} + assert paths["yaml"].read_text(encoding="utf-8").isascii() + return load_yaml(paths["yaml"]), load_graphml(paths["graphml"]) + + +def test_gdart_example_matches_official_witness(tmp_path, witnesslint): + """The sv-witnesses example (produced by GDart) for jpf-regression/ExDarko_false.""" + base = FIXTURES / "sv-witnesses" + inputs = ["./java/common/org/sosy_lab/sv_benchmarks/Verifier.java", "./java/jpf-regression/ExDarko_false/Main.java"] + task = Task(input_files=inputs, property_file=str(VALID_ASSERT), producer_name="GDart", + producer_version="VERSION_PLACEHOLDER", base_dir=base) + main = Location(class_name="Main", source_file="Main.java", line=34, call_index=0) + values = [ + NondetReturn("nondetInt", "int", "0", main), + NondetReturn("nondetInt", "int", "0", Location(class_name="Main", source_file="Main.java", line=35, call_index=0)), + ] + violation = Violation("java.lang.AssertionError", [Frame("Main", "Main.java", 48), Frame("Main", "Main.java", 35)]) + paths = write_witnesses(task, values, violation, tmp_path) + ours = load_yaml(paths["yaml"]) + official = yaml.safe_load((base / "ExDarko_false.witness-2.3.yml").read_text(encoding="utf-8"))[0] + assert ours["content"] == official["content"] + assert ours["metadata"]["task"] == official["metadata"]["task"] + assert ours["metadata"]["producer"] == official["metadata"]["producer"] + assert_single_final_target(ours) + witnesslint(paths["yaml"], base / "java" / "common", base / "java" / "jpf-regression" / "ExDarko_false") + + +def test_return2_from_markers(tmp_path, witnesslint): + """jbmc-regression/return2 with the markers SWAT printed for it.""" + task_dir = FIXTURES / "jbmc-regression" / "return2" + values, violation = parse_markers( + "[WITNESS] LTFAQEAxM0BAQE1haW5AQEBtYWluKFtMamF2YS9sYW5nL1N0cmluZzspVkBAQDBAQEBJQEBATWFpbi5qYXZh\n" + "[WITNESS] MEBAQDE0QEBATWFpbkBAQG1haW4oW0xqYXZhL2xhbmcvU3RyaW5nOylWQEBAMEBAQElAQEBNYWluLmphdmE=\n" + "[WITNESS_TARGET] amF2YS5sYW5nLkFzc2VydGlvbkVycm9yQEBATWFpbkBATWFpbi5qYXZhQEAxNQ==\n" + ) + entry, (data, nodes, edges) = write(task_for(task_dir), values, violation, tmp_path) + md = entry["metadata"]["task"] + assert md["input_files"] == [f"{COMMON}/org/sosy_lab/sv_benchmarks/Verifier.java", f"{task_dir}/Main.java"] + assert md["specification"] == "CHECK( init(Main.main()), LTL(G assert) )" + wps = waypoints(entry) + assert [(wp["type"], wp["location"]["line"], wp["location"]["column"]) for wp in wps] == [ + ("function_return", 13, 33), ("function_return", 14, 33), ("target", 15, 5)] + assert [wp["constraint"]["value"] for wp in wps[:2]] == ["\\result == -1", "\\result == 0"] + assert_single_final_target(entry) + assert data["producer"] == "test" and data["sourcecodelang"] == "Java" + assert [(e["originfile"], e["startline"], e["assumption"], e["assumption.scope"]) for e in edges] == [ + ("Main.java", "13", "\\result = -1;", "java::LMain;.main([Ljava/lang/String;)V"), + ("Main.java", "14", "\\result = 0;", "java::LMain;.main([Ljava/lang/String;)V"), + ] + assert nodes["n0"] == {"entry": "true"} and nodes["n2"] == {"violation": "true"} + witnesslint(tmp_path / "witness.yml", COMMON, task_dir) + + +def test_float_special_values(tmp_path, witnesslint): + """jdart-regression/float: NaN, infinities and negative zero.""" + task_dir = FIXTURES / "jdart-regression" / "float" + loc = lambda line: Location(class_name="Main", source_file="Main.java", line=line, call_index=0, method="main([Ljava/lang/String;)V") + values = [ + NondetReturn("nondetFloat", "float", "-Infinity", loc(12)), + NondetReturn("nondetFloat", "float", "NaN", loc(13)), + NondetReturn("nondetFloat", "float", "-0.0", loc(12)), + NondetReturn("nondetFloat", "float", "Infinity", loc(13)), + NondetReturn("nondetFloat", "float", "1.0E10", loc(12)), + NondetReturn("nondetFloat", "F", "-0.0012512209359556437", loc(13)), + NondetReturn("nondetFloat", "float", "", loc(12), bits="0x3dcccccd"), + ] + violation = Violation("java.lang.AssertionError", [Frame("Main", "Main.java", 14)]) + entry, (_, _, edges) = write(task_for(task_dir), values, violation, tmp_path) + wps = waypoints(entry) + assert [wp["constraint"]["value"] for wp in wps[:-1]] == [ + "\\result == Float.NEGATIVE_INFINITY", "Float.isNaN(\\result)", "\\result == -0.0f", + "\\result == Float.POSITIVE_INFINITY", "\\result == 10000000000.0f", "\\result == -0.0012512209359556437f", + "\\result == 0.10000000149011612f", + ] + assert all(wp["location"]["column"] == 36 for wp in wps[:-1]) + assert wps[-1]["location"] == {"file_name": f"{task_dir}/Main.java", "line": 14, "column": 5} + assert [e["assumption"] for e in edges] == [ + "\\result = -Infinity;", "\\result = NaN;", "\\result = -0.0;", "\\result = Infinity;", + "\\result = 10000000000;", "\\result = -0.0012512209359556437;", "\\result = 0.10000000149011612;", + ] + assert_single_final_target(entry) + witnesslint(tmp_path / "witness.yml", COMMON, task_dir) + + +def test_string_value_and_runtime_exception_thrown_in_jdk(tmp_path, witnesslint): + """jbmc-regression/StringValueOf08: the NumberFormatException is thrown inside the JDK, + the target is the innermost frame of the task sources.""" + task_dir = FIXTURES / "jbmc-regression" / "StringValueOf08" + values = [NondetReturn("nondetString", "String", "fixed", Location(class_name="Main", source_file="Main.java", line=13, call_index=0))] + violation = Violation("java.lang.NumberFormatException", [ + Frame("jdk.internal.math.FloatingDecimal", "FloatingDecimal.java", 2054), + Frame("jdk.internal.math.FloatingDecimal", "FloatingDecimal.java", 122), + Frame("java.lang.Float", "Float.java", 476), + Frame("Main", "Main.java", 14), + ]) + entry, (_, _, edges) = write(task_for(task_dir, NO_RUNTIME_EXCEPTION), values, violation, tmp_path) + assert entry["metadata"]["task"]["specification"] == "CHECK(init(Main.main()), LTL(G ! uncaught(java.lang.RuntimeException)))" + wps = waypoints(entry) + assert wps[0]["constraint"]["value"] == '\\result.equals("fixed")' + assert wps[0]["location"]["column"] == 40 + assert wps[1] == {"type": "target", "action": "follow", "location": {"file_name": f"{task_dir}/Main.java", "line": 14, "column": 5}} + assert edges[0]["assumption"] == '\\result.equals("fixed");' + assert_single_final_target(entry) + witnesslint(tmp_path / "witness.yml", COMMON, task_dir) + + +def test_multi_file_task_with_package(tmp_path, witnesslint): + """MinePump/spec1-5_product25: several files, a package directory, the assertion fails in Actions.java.""" + task_dir = FIXTURES / "MinePump" / "spec1-5_product25" + loc = Location(class_name="Main", source_file="Main.java", line=20, call_index=0, method="getBoolean()Z") + values = [NondetReturn("nondetBoolean", "boolean", v, loc) for v in ("false", "true", "false")] + violation = Violation("java.lang.AssertionError", [ + Frame("Actions", "Actions.java", 104), Frame("Actions", "Actions.java", 50), Frame("Main", "Main.java", 50), Frame("Main", "Main.java", 16)]) + entry, (_, _, edges) = write(task_for(task_dir), values, violation, tmp_path) + files = entry["metadata"]["task"]["input_files"] + assert files == [ + f"{COMMON}/org/sosy_lab/sv_benchmarks/Verifier.java", + f"{task_dir}/Actions.java", f"{task_dir}/Main.java", + f"{task_dir}/MinePumpSystem/Environment.java", f"{task_dir}/MinePumpSystem/MinePump.java", + ] + wps = waypoints(entry) + assert [wp["constraint"]["value"] for wp in wps[:3]] == ["\\result == false", "\\result == true", "\\result == false"] + assert all(wp["location"] == {"file_name": f"{task_dir}/Main.java", "line": 20, "column": 35} for wp in wps[:3]) + assert wps[3]["location"] == {"file_name": f"{task_dir}/Actions.java", "line": 104, "column": 7} + assert_single_final_target(entry) + assert edges[0]["assumption.scope"] == "java::LMain;.getBoolean()Z" + witnesslint(tmp_path / "witness.yml", COMMON, task_dir) + + # a frame in the package directory resolves through the package path + violation = Violation("java.lang.RuntimeException", [Frame("MinePumpSystem.MinePump", "MinePump.java", 30)]) + entry, _ = write(task_for(task_dir), [], violation, tmp_path) + assert waypoints(entry)[0]["location"]["file_name"] == f"{task_dir}/MinePumpSystem/MinePump.java" + # ... and not when the package does not match + violation = Violation("java.lang.RuntimeException", [Frame("other.MinePump", "MinePump.java", 30)]) + entry, _ = write(task_for(task_dir), [], violation, tmp_path) + assert entry["content"] == [] + + +def test_explicit_throw_target(tmp_path): + task_dir = FIXTURES / "jbmc-regression" / "exceptions16" + values = [NondetReturn("nondetInt", "int", "7", Location(class_name="Main", source_file="Main.java", line=19, call_index=0))] + violation = Violation("B", [Frame("Main", "Main.java", 27)]) + entry, _ = write(task_for(task_dir), values, violation, tmp_path) + wps = waypoints(entry) + assert wps[0]["location"]["column"] == 32 + assert wps[1]["location"] == {"file_name": f"{task_dir}/Main.java", "line": 27, "column": 7} + + +def test_inner_class_same_line_calls_unicode_and_package(tmp_path, witnesslint): + task_dir = FIXTURES / "synthetic" + main = lambda line, idx=0, cls="Main", src="Main.java": Location(class_name=cls, source_file=src, line=line, call_index=idx) + values = [ + NondetReturn("nondetChar", "char", "39", main(14)), + NondetReturn("nondetDouble", "double", "1.0E-5", main(15)), + NondetReturn("nondetFloat", "float", "0.1", main(16)), + NondetReturn("nondetLong", "long", "-9223372036854775808", main(17)), + NondetReturn("nondetString", "String", "a\"b<ä&\U0001F600\x00\n", main(18)), + NondetReturn("nondetInt", "int", "1", main(9, 0, "Main$Inner")), + NondetReturn("nondetInt", "int", "2", main(9, 1, "Main$Inner")), + NondetReturn("nondetInt", "int", "2", main(9, 2, "Main$Inner")), # no third call on the line + NondetReturn("nondetInt", "int", "3", main(7, 0, "pkg.Helper", "Helper.java")), + NondetReturn("nondetBoolean", "boolean", "true", main(29, 0, "Companion")), + ] + violation = Violation("java.lang.IllegalStateException", [Frame("Main", "Main.java", 21)]) + entry, (_, _, edges) = write(task_for(task_dir), values, violation, tmp_path) + wps = waypoints(entry) + main_java, helper_java = f"{task_dir}/Main.java", f"{task_dir}/pkg/Helper.java" + assert [(wp["constraint"]["value"], wp["location"]["file_name"], wp["location"]["line"], wp["location"].get("column")) for wp in wps[:-1]] == [ + ("\\result == '\\''", main_java, 14, 34), + ("\\result == 0.000010", main_java, 15, 38), + ("\\result == 0.1f", main_java, 16, 36), + ("\\result == -9223372036854775808L", main_java, 17, 34), + ('\\result.equals("a\\"b<\\u00e4&\\ud83d\\ude00\\u0000\\n")', main_java, 18, 38), + ("\\result == 1", main_java, 9, 33), + ("\\result == 2", main_java, 9, 56), + ("\\result == 2", main_java, 9, None), + ("\\result == 3", helper_java, 7, 35), + ("\\result == true", main_java, 29, 35), + ] + assert wps[-1] == {"type": "target", "action": "follow", "location": {"file_name": main_java, "line": 21, "column": 7}} + assert_single_final_target(entry) + assert edges[4]["assumption"] == '\\result.equals("a\\"b<\\u00e4&\\ud83d\\ude00\\u0000\\n");' + assert edges[5]["assumption.scope"] == "java::LMain$Inner;" + assert edges[8]["originfile"] == "pkg/Helper.java" and edges[8]["assumption.scope"] == "java::Lpkg/Helper;" + assert edges[0]["assumption"] == "\\result = 39;" + witnesslint(tmp_path / "witness.yml", COMMON, task_dir) + + +def test_file_name_locations(tmp_path, caplog): + task_dir = FIXTURES / "synthetic" + values = [ + NondetReturn("nondetChar", "char", "97", Location(file_name=f"{task_dir}/Main.java", line=14, column=34)), + NondetReturn("nondetDouble", "double", "1.5", Location(file_name="Main.java", line=15)), + NondetReturn("nondetInt", "int", "3", Location(file_name="pkg/Helper.java", line=7, column=0)), + ] + entry, (_, _, edges) = write(task_for(task_dir), values, None, tmp_path) + wps = waypoints(entry) + assert [(wp["location"]["file_name"], wp["location"].get("column")) for wp in wps] == [ + (f"{task_dir}/Main.java", 34), (f"{task_dir}/Main.java", 38), (f"{task_dir}/pkg/Helper.java", 35)] + assert [e["assumption.scope"] for e in edges] == ["java::LMain;", "java::LMain;", "java::LHelper;"] + assert [e["originfile"] for e in edges] == ["Main.java", "Main.java", "pkg/Helper.java"] + # a file that is not among the inputs is written as given, with a warning + values = [NondetReturn("nondetInt", "int", "4", Location(file_name="Elsewhere.java", line=1, column=2))] + with caplog.at_level(logging.WARNING, logger="witnesscreator"): + text = witness_module.render_yaml(task_for(task_dir), values, None) + assert "Elsewhere.java is not among the input files" in caplog.text + wp = yaml.safe_load(text)[0]["content"][0]["segment"][0]["waypoint"] + assert wp["location"] == {"file_name": "Elsewhere.java", "line": 1, "column": 2} + + +def test_no_target_warns_but_writes(tmp_path, caplog, witnesslint): + task_dir = FIXTURES / "jbmc-regression" / "return2" + values = [NondetReturn("nondetInt", "int", "1", Location(class_name="Main", source_file="Main.java", line=13))] + with caplog.at_level(logging.WARNING, logger="witnesscreator"): + entry, _ = write(task_for(task_dir), values, None, tmp_path) + assert [wp["type"] for wp in waypoints(entry)] == ["function_return"] + assert "no target waypoint" in caplog.text + witnesslint(tmp_path / "witness.yml", COMMON, task_dir) + + # all frames outside the task sources (JDK, SWAT's own classes, wrong line, unknown class) + caplog.clear() + violation = Violation("java.lang.ArithmeticException", [ + Frame("java.lang.Integer", "Integer.java", 1), Frame("de.uzl.its.swat.Main", "Main.java", 13), + Frame("Main", "Main.java", 0), Frame("Main", "Main.java", 999), Frame("Nope", "Nope.java", 3), Frame("", None, 3)]) + with caplog.at_level(logging.WARNING, logger="witnesscreator"): + entry, _ = write(task_for(task_dir), values, violation, tmp_path) + assert [wp["type"] for wp in waypoints(entry)] == ["function_return"] + assert "No stack frame of java.lang.ArithmeticException lies in the task sources" in caplog.text + + +def test_unresolvable_value_location_falls_back_to_package_path(tmp_path, caplog): + task_dir = FIXTURES / "jbmc-regression" / "return2" + values = [NondetReturn("nondetInt", "int", "1", Location(class_name="securibench/micro/basic/Basic4", source_file="Basic4.java", line=29, method="doGet()V"))] + with caplog.at_level(logging.WARNING, logger="witnesscreator"): + text = witness_module.render_yaml(task_for(task_dir), values, None) + assert "Could not find the source of securibench/micro/basic/Basic4" in caplog.text + doc = yaml.safe_load(text)[0] + wp = doc["content"][0]["segment"][0]["waypoint"] + assert wp["location"] == {"file_name": "securibench/micro/basic/Basic4.java", "line": 29} + assert wp["constraint"]["value"] == "\\result == 1" + + +def test_never_crashes_on_garbage(tmp_path, caplog): + task = Task(input_files=[str(tmp_path / "missing"), "", str(FIXTURES / "synthetic")], property_file=str(tmp_path / "no.prp")) + values = [ + NondetReturn(None, None, None, None), + NondetReturn("x", "weird", "v", Location(line="x")), + NondetReturn("x", "int", "1", Location(class_name="Main", line=-4)), + NondetReturn("x", "int", "1", Location(line=3)), + NondetReturn("x", "int", object(), Location(class_name="Main", source_file="Main.java", line=14, call_index=None, column="?")), + "not even a value", + ] + violation = Violation(None, [None, "junk", Frame(None, None, None), Frame("Main", "Main.java", "x")]) + with caplog.at_level(logging.WARNING, logger="witnesscreator"): + paths = write_witnesses(task, values, violation, tmp_path) + assert set(paths) == {"yaml", "graphml"} + entry = load_yaml(paths["yaml"]) + assert entry["metadata"]["task"]["specification"] == witness_module.DEFAULT_SPECIFICATION + assert len(entry["content"]) == 1 # the one value with a usable location + load_graphml(paths["graphml"]) + assert "Ignoring missing input" in caplog.text + assert "Could not read property file" in caplog.text + + +def test_formats_are_written_independently(tmp_path, monkeypatch): + task_dir = FIXTURES / "jbmc-regression" / "return2" + + def boom(*args, **kwargs): + raise RuntimeError("graphml writer broken") + + monkeypatch.setattr(witness_module, "_render_graphml", boom) + paths = write_witnesses(task_for(task_dir), [], None, tmp_path) + assert set(paths) == {"yaml"} + assert write_witnesses(task_for(task_dir), [], None, tmp_path, formats=("yaml",)) == {"yaml": tmp_path / "witness.yml"} + assert write_witnesses(task_for(task_dir), [], None, tmp_path, formats=("xml",)) == {} + assert write_witnesses(task_for(task_dir), [], None, tmp_path, formats=()) == {} + + +def test_render_functions_and_relative_inputs(tmp_path): + task = Task(input_files=["../common/", "return2/Main.java"], property_file="../properties/valid-assert.prp", + base_dir=FIXTURES / "jbmc-regression") + values = [NondetReturn("nondetInt", "int", "1", Location(class_name="Main", source_file="Main.java", line=13))] + doc = yaml.safe_load(witness_module.render_yaml(task, values, None))[0] + assert doc["metadata"]["task"]["input_files"] == ["../common/org/sosy_lab/sv_benchmarks/Verifier.java", "return2/Main.java"] + assert doc["metadata"]["producer"]["name"] == "witnesscreator" + assert doc["content"][0]["segment"][0]["waypoint"]["location"]["file_name"] == "return2/Main.java" + assert "Main.java" in witness_module.render_graphml(task, values, None) + + +@pytest.mark.parametrize("name", ["jbmc-regression/return2", "jdart-regression/float", "MinePump/spec1-5_product25", "synthetic"]) +def test_default_violation_lints(tmp_path, witnesslint, name): + task_dir = FIXTURES / name + paths = write_witnesses(task_for(task_dir), [], None, tmp_path) + assert load_yaml(paths["yaml"])["content"] == [] + witnesslint(paths["yaml"], COMMON, task_dir) diff --git a/tests/test_escaping.py b/tests/test_escaping.py new file mode 100644 index 0000000..721da06 --- /dev/null +++ b/tests/test_escaping.py @@ -0,0 +1,102 @@ +import xml.etree.ElementTree as ET + +import pytest +import yaml + +from witnesscreator.graphml_writer import Assumption, render_graphml, xml_escape +from witnesscreator.yaml_writer import Metadata, Waypoint, render_yaml, yaml_quote + + +@pytest.mark.parametrize( + "s", + [ + "", + "plain", + 'quote " and backslash \\', + "\\result == 3", + "tab\tnewline\ncr\r", + "\x00\x01\x1f\x7f", + "\x85\xa0

", + "ä€", + "\U0001F600 emoji", + "￾￿", + "\ud800 lone surrogate", + "a: b # not a comment", + "- not a list", + ], +) +def test_yaml_quote_round_trips_through_pyyaml(s): + quoted = yaml_quote(s) + assert quoted.isascii() + assert yaml.safe_load(quoted) == s + assert yaml.safe_load(f"k: {quoted}\n") == {"k": s} + + +def test_yaml_quote_escapes(): + assert yaml_quote('\\result.equals("a\\"b")') == '"\\\\result.equals(\\"a\\\\\\"b\\")"' + assert yaml_quote("ä") == '"\\u00e4"' + assert yaml_quote("\U0001F600") == '"\\U0001f600"' + assert yaml_quote("\n\t\r") == '"\\n\\t\\r"' + + +def test_yaml_document_structure(): + md = Metadata("u", "2026-01-01T00:00:00Z", "P", "v", 'CHECK( init(Main.main()), LTL(G assert) )', [("a/Main.java", "0" * 64)]) + text = render_yaml(md, [Waypoint.function_return('\\result.equals("x")', "a/Main.java", 3, 7), Waypoint.target("a/Main.java", 4, None)]) + doc = yaml.safe_load(text) + entry = doc[0] + assert entry["metadata"]["uuid"] == "u" + assert entry["metadata"]["task"]["input_file_hashes"] == {"a/Main.java": "0" * 64} + assert entry["metadata"]["task"]["specification"] == "CHECK( init(Main.main()), LTL(G assert) )" + wps = [s["segment"][0]["waypoint"] for s in entry["content"]] + assert wps[0]["constraint"] == {"value": '\\result.equals("x")', "format": "java_expression"} + assert wps[0]["location"] == {"file_name": "a/Main.java", "line": 3, "column": 7} + assert wps[1] == {"type": "target", "action": "follow", "location": {"file_name": "a/Main.java", "line": 4}} + assert yaml.safe_load(render_yaml(md, []))[0]["content"] == [] + empty = Metadata("u", "t", "P", "v", "spec", []) + assert yaml.safe_load(render_yaml(empty, []))[0]["metadata"]["task"]["input_files"] == [] + + +@pytest.mark.parametrize( + "s, expected", + [ + ("a < b & c > d \"q\"", "a < b & c > d "q""), + ("", "<init>"), + ("\x00\x01\x0b", "\\u0000\\u0001\\u000b"), + ("\t\n ok", "\t\n ok"), + ("￾", "\\ufffe"), + ("ä", "ä"), + ], +) +def test_xml_escape(s, expected): + assert xml_escape(s) == expected + + +def test_graphml_escapes_all_text_and_parses(): + text = render_graphml( + "P & Co", "CHECK( )", + [Assumption("pkg/

.java", 3, '\\result.equals("");', "java::Lpkg/Main;.()V")], + ) + root = ET.fromstring(text) + ns = "{http://graphml.graphdrawing.org/xmlns}" + graph = root.find(ns + "graph") + data = {d.get("key"): d.text for d in graph.findall(ns + "data")} + assert data["producer"] == "P & Co" + assert data["specification"] == "CHECK( )" + assert data["sourcecodelang"] == "Java" + edge = graph.find(ns + "edge") + ed = {d.get("key"): d.text for d in edge.findall(ns + "data")} + assert ed["originfile"] == "pkg/
.java" + assert ed["assumption"] == '\\result.equals("");' + assert ed["assumption.scope"] == "java::Lpkg/Main;.()V" + nodes = {n.get("id"): {d.get("key"): d.text for d in n.findall(ns + "data")} for n in graph.findall(ns + "node")} + assert nodes["n0"] == {"entry": "true"} + assert nodes["n1"] == {"violation": "true"} + assert "sink" in nodes + + +def test_graphml_default_violation(): + root = ET.fromstring(render_graphml("P", None, [])) + ns = "{http://graphml.graphdrawing.org/xmlns}" + nodes = {n.get("id"): {d.get("key"): d.text for d in n.findall(ns + "data")} for n in root.iter(ns + "node")} + assert nodes["n0"] == {"entry": "true", "violation": "true"} + assert not list(root.iter(ns + "edge")) diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..7fcaf06 --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,62 @@ +import json + +from witnesscreator import Frame, Location, NondetReturn, Violation, dump_events, parse_events, read_events +from witnesscreator.events import nondet_return_from_dict, nondet_return_to_dict, violation_to_dict + + +def test_round_trip(tmp_path): + values = [ + NondetReturn("nondetInt", "int", "42", Location(class_name="pkg.Main", source_file="Main.java", line=12, call_index=1)), + NondetReturn("nondetString", "String", "a\"b\nä", Location(file_name="Main.java", line=13, column=27), bits=None), + NondetReturn("nondetFloat", "float", "0.1", Location(class_name="Main", line=14), bits="0x3dcccccd"), + ] + violation = Violation("java.lang.AssertionError", [Frame("pkg.Main", "Main.java", 17), Frame("java.lang.Thread", None, -1)]) + text = dump_events(values, violation, "SWAT", "svcomp27") + lines = text.splitlines() + header = json.loads(lines[0]) + assert header == {"kind": "header", "schema": "witnesscreator-events", "version": 1, "producer": {"name": "SWAT", "version": "svcomp27"}} + assert json.loads(lines[1])["kind"] == "nondet_return" + assert json.loads(lines[1])["location"]["class_name"] == "pkg.Main" + assert json.loads(lines[-1])["kind"] == "violation" + + path = tmp_path / "events.jsonl" + path.write_text(text, encoding="utf-8") + ev = read_events(path) + assert ev.producer_name == "SWAT" and ev.producer_version == "svcomp27" + assert ev.values == values + assert ev.violation == violation + + +def test_dict_forms(): + nr = NondetReturn("nondetInt", "int", "1", Location(line=3)) + d = nondet_return_to_dict(nr) + assert d["kind"] == "nondet_return" and d["bits"] is None and d["location"]["line"] == 3 + assert nondet_return_from_dict(d) == nr + v = violation_to_dict(Violation("x", [])) + assert v == {"kind": "violation", "throwable": "x", "frames": []} + + +def test_lenient_parsing(): + lines = [ + "", + "not json", + "[1, 2]", + '{"kind": "header", "schema": "other", "version": 99}', + '{"kind": "nondet_return", "type": "int", "value": 7, "location": {"line": "4", "call_index": null, "column": 0}}', + '{"kind": "nondet_return", "value": "x"}', + '{"kind": "violation", "throwable": "A", "frames": [{"class_name": "Main", "line": "x"}, "junk"]}', + '{"kind": "violation", "throwable": "B", "frames": null}', + '{"kind": "mystery"}', + ] + ev = parse_events(lines) + assert ev.header["version"] == 99 + assert ev.producer_name is None + assert len(ev.values) == 2 + assert ev.values[0].value == "7" and ev.values[0].location.line == 4 + assert ev.values[0].location.call_index == 0 and ev.values[0].location.column is None + assert ev.values[1].location == Location() + assert ev.violation.throwable == "B" and ev.violation.frames == [] + + +def test_header_without_producer(): + assert json.loads(dump_events([], None).splitlines()[0]) == {"kind": "header", "schema": "witnesscreator-events", "version": 1} diff --git a/tests/test_literals.py b/tests/test_literals.py new file mode 100644 index 0000000..b904d4a --- /dev/null +++ b/tests/test_literals.py @@ -0,0 +1,141 @@ +import pytest + +from witnesscreator.literals import ( + char_literal, + graphml_assumption_value, + plain_decimal, + result_constraint, + string_literal, + value_from_bits, +) +from witnesscreator.model import normalize_type + + +def test_integral_types(): + assert result_constraint("5", "int") == "\\result == 5" + assert result_constraint("-5", "I") == "\\result == -5" + assert result_constraint("-128", "byte") == "\\result == -128" + assert result_constraint("32767", "S") == "\\result == 32767" + assert result_constraint("-2147483648", "int") == "\\result == -2147483648" + + +def test_longs_get_suffix_only_outside_int_range(): + assert result_constraint("5", "long") == "\\result == 5" + assert result_constraint("2147483647", "J") == "\\result == 2147483647" + assert result_constraint("2147483648", "J") == "\\result == 2147483648L" + assert result_constraint("5000000000", "long") == "\\result == 5000000000L" + assert result_constraint("-9223372036854775808", "long") == "\\result == -9223372036854775808L" + assert result_constraint("garbage", "long") == "\\result == garbage" + + +def test_booleans(): + assert result_constraint("true", "boolean") == "\\result == true" + assert result_constraint("false", "Z") == "\\result == false" + assert result_constraint("maybe", "boolean") == "\\result == false" + + +def test_chars_are_java_literals(): + assert result_constraint("97", "char") == "\\result == 'a'" + assert result_constraint("39", "C") == "\\result == '\\''" + assert result_constraint("92", "C") == "\\result == '\\\\'" + assert result_constraint("10", "C") == "\\result == '\\n'" + assert result_constraint("0", "C") == "\\result == '\\u0000'" + assert result_constraint("228", "C") == "\\result == '\\u00e4'" + assert result_constraint("65535", "C") == "\\result == '\\uffff'" + assert result_constraint("127", "C") == "\\result == '\\u007f'" + assert char_literal("x") == "x" + assert char_literal("70000") == "70000" + + +def test_floating_point(): + assert result_constraint("1.5", "float") == "\\result == 1.5f" + assert result_constraint("-0.000002022784", "F") == "\\result == -0.000002022784f" + assert result_constraint("1.0E10", "F") == "\\result == 10000000000.0f" + assert result_constraint("1.0E-5", "D") == "\\result == 0.000010" + assert result_constraint("0.1", "double") == "\\result == 0.1" + assert result_constraint("-0.0", "D") == "\\result == -0.0" + assert result_constraint("-0.0", "F") == "\\result == -0.0f" + assert result_constraint("10000000000", "D") == "\\result == 10000000000.0" + assert result_constraint("NaN", "float") == "Float.isNaN(\\result)" + assert result_constraint("NaN", "D") == "Double.isNaN(\\result)" + assert result_constraint("Infinity", "double") == "\\result == Double.POSITIVE_INFINITY" + assert result_constraint("-Infinity", "float") == "\\result == Float.NEGATIVE_INFINITY" + assert result_constraint("1.5f", "F") == "\\result == 1.5f" + assert result_constraint("nonsense", "D") == "\\result == nonsense.0" + + +def test_plain_decimal(): + assert plain_decimal("1.0E10") == "10000000000" + assert plain_decimal("-1.0E-3") == "-0.0010" + assert plain_decimal("-0.0") == "-0.0" + assert plain_decimal("1E3") == "1000" + assert plain_decimal("x") == "x" + + +def test_strings(): + assert result_constraint("abc", "String") == '\\result.equals("abc")' + assert result_constraint("", "Ljava/lang/String;") == '\\result.equals("")' + assert result_constraint('a"b\\c\nd\te', "String") == '\\result.equals("a\\"b\\\\c\\nd\\te")' + assert result_constraint("\u0001x\u007f", "String") == '\\result.equals("\\u0001x\\u007f")' + assert result_constraint("\r\b\f", "String") == '\\result.equals("\\r\\b\\f")' + + +def test_string_literal_escapes_everything_non_ascii(): + assert string_literal("ä€") == '"\\u00e4\\u20ac"' + # outside the BMP: a surrogate pair, as Java sees it + assert string_literal("\U0001F600") == '"\\ud83d\\ude00"' + # a lone surrogate (e.g. from surrogateescape decoding) is representable too + assert string_literal("\ud800") == '"\\ud800"' + assert string_literal("\x00\x1f\x85
") == '"\\u0000\\u001f\\u0085\\u2028"' + + +def test_legacy_markers_without_type(): + assert result_constraint("42", None) == "\\result == 42" + assert result_constraint("-7", None) == "\\result == -7" + assert result_constraint("true", None) == "\\result == true" + assert result_constraint("hi", None) == '\\result.equals("hi")' + assert result_constraint("1.5", "unknown-type") == '\\result.equals("1.5")' + + +def test_normalize_type(): + assert normalize_type("I") == "int" + assert normalize_type("int") == "int" + assert normalize_type("Ljava/lang/String;") == "String" + assert normalize_type("java.lang.String") == "String" + assert normalize_type(None) is None + assert normalize_type("Object") is None + + +def test_value_from_bits(): + assert value_from_bits("0x3dcccccd", "float") == "0.10000000149011612" + assert value_from_bits("0x3ff0000000000000", "double") == "1.0" + assert value_from_bits("0x7fc00000", "float") == "NaN" + assert value_from_bits("0x7f800000", "float") == "Infinity" + assert value_from_bits("0xfff0000000000000", "double") == "-Infinity" + assert value_from_bits("0x80000000", "float") == "-0.0" + assert value_from_bits("0x1", "int") is None + assert value_from_bits("zz", "float") is None + assert result_constraint(value_from_bits("0x3dcccccd", "float"), "float") == "\\result == 0.10000000149011612f" + + +@pytest.mark.parametrize( + "value, type_, expected", + [ + ("5", "int", "5"), + ("5000000000", "long", "5000000000"), + ("true", "boolean", "true"), + ("97", "char", "97"), + ("a", "char", "97"), + ("1.5", "float", "1.5"), + ("1.0E10", "double", "10000000000"), + ("-0.0", "double", "-0.0"), + ("NaN", "float", "NaN"), + ("-Infinity", "double", "-Infinity"), + ("a\"b", "String", '.equals("a\\"b")'), + ("ä", "String", '.equals("\\u00e4")'), + ("42", None, "42"), + ("x", None, '.equals("x")'), + ], +) +def test_graphml_assumption_value(value, type_, expected): + assert graphml_assumption_value(value, type_) == expected diff --git a/tests/test_locations.py b/tests/test_locations.py new file mode 100644 index 0000000..11eed7f --- /dev/null +++ b/tests/test_locations.py @@ -0,0 +1,173 @@ +import pytest + +from witnesscreator.columns import mask_non_code, nondet_call_column, statement_column +from witnesscreator.sources import SourceFiles, fallback_file_name, split_lines + + +def test_column_of_closing_parenthesis(): + line = " Boolean input_1_0 = Verifier.nondetBoolean();" + assert nondet_call_column(line, 0) == 48 + assert nondet_call_column(line, 1) is None + two = " if (!chunked && Verifier.nondetInt() > 0 && Verifier.nondetInt() != 0) {" + c0, c1 = nondet_call_column(two, 0), nondet_call_column(two, 1) + assert two[c0 - 1] == ")" and two[c1 - 1] == ")" and c0 < c1 + assert nondet_call_column("x = Verifier.nondetInt ( );", 0) == 26 + assert nondet_call_column("no call here", 0) is None + assert nondet_call_column("Verifier.nondetInt(", 0) is None + assert nondet_call_column("x = nondetInt(f(1, (2)));", 0) == 24 + assert nondet_call_column("", 0) is None + assert nondet_call_column(None, 0) is None + assert nondet_call_column("Verifier.nondetInt()", -1) is None + + +def test_nondet_call_needs_word_boundary(): + assert nondet_call_column("x = mynondetInt();", 0) is None + assert nondet_call_column("x = my_nondetInt();", 0) is None + assert nondet_call_column("x = Verifier.nondetInt();", 0) == 24 + assert nondet_call_column("x = nondet();", 0) == 12 + + +def test_calls_in_comments_and_strings_are_ignored(): + line = mask_non_code('String s = "nondetInt()"; int x = Verifier.nondetInt(); // nondetInt()') + assert nondet_call_column(line, 0) == 54 + assert nondet_call_column(line, 1) is None + line = mask_non_code('int x = Verifier.nondetInt(/* ) */);') + assert nondet_call_column(line, 0) == 35 + line = mask_non_code("char c = ')'; int x = Verifier.nondetInt();") + assert nondet_call_column(line, 0) == 42 + + +def test_mask_non_code_keeps_positions(): + src = 'a = "x\\"y"; // c\nb = \'"\'; /* multi\nline */ c = 1;\nd = """t\nb""" + 2;\n' + masked = mask_non_code(src) + assert len(masked) == len(src) + assert masked.count("\n") == src.count("\n") + assert masked.splitlines() == ["a = ; ", "b = ; ", " c = 1;", "d = ", " + 2;"] + # unterminated constructs do not raise + assert len(mask_non_code('x = "unterminated')) == len('x = "unterminated') + assert len(mask_non_code("/* never closed")) == len("/* never closed") + assert mask_non_code("") == "" + + +def test_column_of_violating_statement(): + assert statement_column(" } else assert false;", True) == 12 + assert statement_column(" assert !(output_1 == 1);", True) == 5 + assert statement_column(" assert !(output_1 == 1);", False) == 5 + assert statement_column(" } throw new RuntimeException();", False) == 7 + assert statement_column(" int x = a / b;", False) == 5 + assert statement_column(" } else { int x = a / b; }", False) == 14 + assert statement_column("\t\t\tx = elsewhere / 0;", False) == 4 # a tab is one column + assert statement_column(" }", False) is None + assert statement_column("", False) is None + assert statement_column(None, False) is None + assert statement_column(" elsewhere = 1;", False) == 5 + assert statement_column(" x.throws();", False) == 3 + assert statement_column(" if (b) throw new Error();", True) == 10 + # the keyword inside a string or comment does not count once masked + assert statement_column(mask_non_code(' s = "assert"; assert x;'), True) == 17 + assert statement_column(mask_non_code(" } // assert here"), True) is None + + +def test_split_lines_only_on_line_breaks(): + assert split_lines("a\nb\r\nc\rd") == ["a", "b", "c", "d"] + assert split_lines("a\n") == ["a"] + assert split_lines("") == [] + assert split_lines("a\x0cb
c\n") == ["a\x0cb
c"] + + +@pytest.fixture +def tree(tmp_path): + task = tmp_path / "task" + (task / "pkg").mkdir(parents=True) + (task / "Main.java").write_text("class Main {}\nclass Helper {}\n") + (task / "pkg" / "Foo.java").write_text("package pkg;\nclass Foo {}\n") + (task / "notes.txt").write_text("ignored") + common = tmp_path / "common" + (common / "org" / "sosy_lab" / "sv_benchmarks").mkdir(parents=True) + (common / "org" / "sosy_lab" / "sv_benchmarks" / "Verifier.java").write_text("package org.sosy_lab.sv_benchmarks;\n") + return tmp_path, task, common + + +def test_collect_lists_inputs_as_given(tree): + tmp, task, common = tree + files = SourceFiles.collect([str(common) + "/", str(task)]) + assert [f.display_path for f in files] == [ + f"{common}/org/sosy_lab/sv_benchmarks/Verifier.java", + f"{task}/Main.java", + f"{task}/pkg/Foo.java", + ] + assert [f.relative_path for f in files] == ["org/sosy_lab/sv_benchmarks/Verifier.java", "Main.java", "pkg/Foo.java"] + assert len(files.files[0].sha256) == 64 + # a file reachable through several inputs is listed once + nested = SourceFiles.collect([str(task / "pkg"), str(task), str(task / "pkg" / "Foo.java")]) + assert [f.display_path for f in nested] == [f"{task}/pkg/Foo.java", f"{task}/Main.java"] + # relative inputs are resolved against base_dir but displayed as given + rel = SourceFiles.collect(["../common/", "Main.java"], base_dir=task) + assert [f.display_path for f in rel] == ["../common/org/sosy_lab/sv_benchmarks/Verifier.java", "Main.java"] + assert rel.files[1].path.exists() + # missing and non-java inputs are ignored + assert len(SourceFiles.collect([str(tmp / "missing"), str(task / "notes.txt"), ""])) == 0 + assert len(SourceFiles.collect([str(task / "notes.txt")])) == 0 + + +def test_resolve_classes(tree): + _, task, common = tree + files = SourceFiles.collect([str(common), str(task)]) + assert files.resolve("Main", "Main.java").relative_path == "Main.java" + assert files.resolve("Main$1", None).relative_path == "Main.java" + assert files.resolve("Helper", "Main.java").relative_path == "Main.java" + assert files.resolve("pkg/Foo", "Foo.java").relative_path == "pkg/Foo.java" + assert files.resolve("pkg.Foo$Inner", "").relative_path == "pkg/Foo.java" + assert files.resolve("org.sosy_lab.sv_benchmarks.Verifier", "Verifier.java").relative_path == "org/sosy_lab/sv_benchmarks/Verifier.java" + assert files.resolve("Missing", "") is None + # a file whose package declaration contradicts the class is never taken + assert files.resolve("other.Foo", "Foo.java", allow_name_only=True) is None + assert files.resolve("other.Main", None, allow_name_only=True) is None + assert files.resolve("Foo", None, allow_name_only=True) is None # Foo lives in pkg/ + # a file without a readable package declaration only resolves by name when allowed + (task / "Empty.java").write_text("") + files = SourceFiles.collect([str(common), str(task)]) + assert files.resolve("some.pkg.Empty", None, allow_name_only=True).relative_path == "Empty.java" + assert files.resolve("some.pkg.Empty", None, allow_name_only=False) is None + # a matching declaration is accepted whatever the path (e.g. sources below an src/ dir) + (task / "src").mkdir() + (task / "src" / "Bar.java").write_text("/* c */ package pkg; class Bar {}\n") + files = SourceFiles.collect([str(task)]) + assert files.resolve("pkg.Bar", "Bar.java", allow_name_only=False).relative_path == "src/Bar.java" + assert files.resolve("Bar", "Bar.java", allow_name_only=True) is None + # the package path may be a suffix of the file's path (input root above the package root) + outer = SourceFiles.collect([str(task.parent)]) + assert outer.resolve("pkg.Foo", "Foo.java", allow_name_only=False).relative_path == "task/pkg/Foo.java" + assert outer.resolve("Main", "Main.java", allow_name_only=False).relative_path == "task/Main.java" + + +def test_resolve_file_name(tree): + _, task, common = tree + files = SourceFiles.collect([str(common), str(task)]) + assert files.resolve_file_name(f"{task}/pkg/Foo.java").relative_path == "pkg/Foo.java" + assert files.resolve_file_name("pkg/Foo.java").relative_path == "pkg/Foo.java" + assert files.resolve_file_name("./Foo.java").relative_path == "pkg/Foo.java" + assert files.resolve_file_name("Verifier.java").relative_path == "org/sosy_lab/sv_benchmarks/Verifier.java" + assert files.resolve_file_name("Nope.java") is None + assert files.resolve_file_name("") is None + + +def test_lines_and_masking(tree): + _, task, _ = tree + files = SourceFiles.collect([str(task)]) + main = files.resolve("Main", None) + assert main.line(1) == "class Main {}" + assert main.line(2) == "class Helper {}" + assert main.line(3) is None + assert main.line(0) is None + assert main.masked_line(1) == "class Main {}" + assert main.masked_line(9) is None + assert main.package == "" + assert files.resolve("pkg.Foo", None).package == "pkg" + + +def test_fallback_file_name(): + assert fallback_file_name("pkg.Main$1", "Main.java") == "pkg/Main.java" + assert fallback_file_name("pkg/Main$1", None) == "pkg/Main.java" + assert fallback_file_name("Main", "") == "Main.java" + assert fallback_file_name("a.b.Outer$Inner$2", None) == "a/b/Outer.java" diff --git a/tests/test_markers.py b/tests/test_markers.py new file mode 100644 index 0000000..8c4d54e --- /dev/null +++ b/tests/test_markers.py @@ -0,0 +1,123 @@ +import base64 + +import pytest + +from witnesscreator.markers import ( + decode_base64, + parse_marker_lines, + parse_markers, + parse_target_marker, + parse_value_marker, +) + + +def b64(s: str) -> str: + return base64.b64encode(s.encode()).decode() + + +def test_parses_current_format(): + m = parse_value_marker("5@@@13@@@pkg/Main$1@@@main([Ljava/lang/String;)V@@@2@@@I@@@Main.java") + assert m.value == "5" + assert m.type == "int" + assert m.function == "nondetInt" + assert m.location.line == 13 + assert m.location.class_name == "pkg/Main$1" + assert m.location.method == "main([Ljava/lang/String;)V" + assert m.location.call_index == 2 + assert m.location.source_file == "Main.java" + + +def test_string_values_may_contain_the_separator(): + m = parse_value_marker("a@@@b@@@7@@@Main@@@main()V@@@0@@@Ljava/lang/String;@@@Main.java") + assert m.value == "a@@@b" + assert m.type == "String" + assert m.function == "nondetString" + assert m.location.line == 7 + legacy = parse_value_marker("a@@@b@@@7@@@Main@@@main()V") + assert legacy.value == "a@@@b" + assert legacy.location.line == 7 + + +def test_parses_legacy_format(): + m = parse_value_marker("true@@@13@@@Main@@@main([Ljava/lang/String;)V") + assert m.value == "true" + assert m.type is None + assert m.function is None + assert m.location.line == 13 + assert m.location.class_name == "Main" + assert m.location.call_index == 0 + assert m.location.source_file is None + + +def test_empty_source_file_and_types(): + m = parse_value_marker("97@@@3@@@Main@@@m()V@@@0@@@C@@@") + assert m.type == "char" and m.location.source_file is None + m = parse_value_marker("1.5@@@3@@@Main@@@m()V@@@0@@@F@@@Main.java") + assert m.type == "float" and m.function == "nondetFloat" + + +def test_rejects_garbage(): + with pytest.raises(ValueError): + parse_value_marker("nonsense") + with pytest.raises(ValueError): + parse_value_marker("a@@@b@@@c@@@d") # line is not a number + with pytest.raises(ValueError): + parse_target_marker("no separator") + + +def test_parses_target(): + t = parse_target_marker( + "java.lang.AssertionError@@@Main@@Main.java@@23;pkg.Helper$Inner@@Helper.java@@5;java.lang.Thread@@@@-1" + ) + assert t.is_assertion_error + assert len(t.frames) == 3 + assert t.frames[0].class_name == "Main" + assert t.frames[0].source_file == "Main.java" + assert t.frames[0].line == 23 + assert t.frames[2].source_file is None + assert t.frames[2].line == -1 + assert parse_target_marker("java.lang.RuntimeException@@@").frames == [] + assert parse_target_marker("x@@@bad;Main@@Main.java@@notanumber").frames[0].line == -1 + + +def test_marker_lines_from_log_output(): + lines = [ + "2026-10-15 10:20:02 [INFO] [SWAT] --> [WITNESS] " + b64("3@@@4@@@Main@@@main([Ljava/lang/String;)V@@@0@@@I@@@Main.java"), + "unrelated line", + "[WITNESS] not-base64!!", + "[WITNESS_TARGET] " + b64("java.lang.AssertionError@@@Main@@Main.java@@7"), + "[WITNESS] " + b64("a\"b@@@5@@@Main@@@main([Ljava/lang/String;)V@@@0@@@Ljava/lang/String;@@@Main.java"), + ] + values, violation = parse_marker_lines(lines) + assert [v.value for v in values] == ["3", 'a"b'] + assert violation.throwable == "java.lang.AssertionError" + assert violation.frames[0].line == 7 + + +def test_last_target_wins(): + lines = [ + "[WITNESS_TARGET] " + b64("java.lang.AssertionError@@@Main@@Main.java@@7"), + "[WITNESS_TARGET] " + b64("java.lang.RuntimeException@@@Main@@Main.java@@9"), + ] + _, violation = parse_marker_lines(lines) + assert violation.throwable == "java.lang.RuntimeException" + + +def test_parse_markers_accepts_base64_blob_and_plain_text(): + plain = "[WITNESS] " + b64("1@@@2@@@Main@@@()V") + "\n[WITNESS_TARGET] " + b64("java.lang.AssertionError@@@Main@@Main.java@@3") + for text in (plain, b64(plain), " " + b64(plain) + "\n"): + values, violation = parse_markers(text) + assert len(values) == 1 and violation is not None + assert parse_markers("") == ([], None) + assert parse_markers(" ") == ([], None) + assert parse_markers("not markers at all") == ([], None) + assert parse_markers(None) == ([], None) + + +def test_decode_base64_is_lenient(): + assert decode_base64(b64("abc")) == "abc" + assert decode_base64("YWJj") == "abc" + assert decode_base64("YWJjZA") == "abcd" # missing padding + assert decode_base64("YW Jj\n") == "abc" # whitespace + assert decode_base64(base64.urlsafe_b64encode(b"\xfb\xff").decode()) == "��" # url-safe, bad utf-8 replaced + assert decode_base64("!!!") == "" # nothing but junk decodes to nothing From 3a484d203229c11b781acfe982c5181a9cc48523 Mon Sep 17 00:00:00 2001 From: Nils Loose Date: Thu, 15 Oct 2026 13:34:10 +0000 Subject: [PATCH 4/5] CI: test on Python 3.10/3.13/3.14, lint witnesses, release wheel, sdist and pyz Tests run with WitnessLint from the sv-witnesses repository so the generated witnesses are linted in CI. On a v* tag the wheel, the sdist, witnesscreator.pyz and SHA256SUMS are attached to the GitHub release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 76 +++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a970e1..d2930b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,42 +10,69 @@ on: workflow_dispatch: jobs: - build: + test: runs-on: ubuntu-latest permissions: contents: read + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.13', '3.14'] steps: - uses: actions/checkout@v5 - - name: Set up JDK 17 - uses: actions/setup-java@v4 + - uses: actions/setup-python@v5 with: - java-version: '17' - distribution: 'temurin' + python-version: ${{ matrix.python-version }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4 + - name: Install + run: python -m pip install --upgrade pip && python -m pip install -e '.[test]' - - name: Build release artifact + - name: Install WitnessLint run: | - version="${GITHUB_REF_NAME#v}" - if [[ "$GITHUB_REF_TYPE" != "tag" ]]; then - version="ci-${GITHUB_SHA::7}" - fi - ./gradlew --no-daemon -PwitnessCreatorVersion="$version" clean build + git clone --depth 1 https://gitlab.com/sosy-lab/benchmarking/sv-witnesses.git "$RUNNER_TEMP/sv-witnesses" + python -m pip install -r "$RUNNER_TEMP/sv-witnesses/linter/requirements.txt" + echo "WITNESSLINT=python $RUNNER_TEMP/sv-witnesses/linter/witnesslinter.py" >> "$GITHUB_ENV" + + - name: Test (unit, golden, WitnessLint on generated witnesses) + run: python -m pytest -v -rs + + - name: Smoke-test the command line + run: | + witnesscreator --version + python -m witnesscreator --help > /dev/null + + build: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v5 + with: + python-version: '3.14' - - name: Create checksum + - name: Build wheel, sdist and zipapp run: | - cd build/distributions - sha256sum witness-creator-*.zip > SHA256SUMS + python -m pip install --upgrade pip build + python -m build + python tools/build_pyz.py dist/witnesscreator.pyz + python dist/witnesscreator.pyz --version + python -m pip install dist/witnesscreator-*.whl + witnesscreator --version + cd dist && sha256sum witnesscreator-* > SHA256SUMS && cat SHA256SUMS - name: Upload CI artifact uses: actions/upload-artifact@v4 with: - name: witness-creator-${{ github.sha }} + name: witnesscreator-${{ github.sha }} path: | - build/distributions/witness-creator-*.zip - build/distributions/SHA256SUMS + dist/witnesscreator-*.whl + dist/witnesscreator-*.tar.gz + dist/witnesscreator.pyz + dist/SHA256SUMS if-no-files-found: error release: @@ -60,7 +87,7 @@ jobs: - name: Download CI artifact uses: actions/download-artifact@v4 with: - name: witness-creator-${{ github.sha }} + name: witnesscreator-${{ github.sha }} path: dist - name: Publish GitHub release @@ -68,13 +95,14 @@ jobs: GH_TOKEN: ${{ github.token }} run: | tag="${GITHUB_REF_NAME}" - notes="$(printf 'Automated WitnessCreator release.\n\nCommit: %s\nRun: %s/%s/actions/runs/%s\n' \ - "$GITHUB_SHA" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")" + notes="$(printf 'Automated WitnessCreator release.\n\nInstall: pip install git+https://github.com/%s@%s\n\nCommit: %s\nRun: %s/%s/actions/runs/%s\n' \ + "$GITHUB_REPOSITORY" "$tag" "$GITHUB_SHA" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")" + files=(dist/witnesscreator-*.whl dist/witnesscreator-*.tar.gz dist/witnesscreator.pyz dist/SHA256SUMS) if gh release view "$tag" >/dev/null 2>&1; then - gh release upload "$tag" dist/witness-creator-*.zip dist/SHA256SUMS --clobber + gh release upload "$tag" "${files[@]}" --clobber else - gh release create "$tag" dist/witness-creator-*.zip dist/SHA256SUMS \ + gh release create "$tag" "${files[@]}" \ --verify-tag \ --title "WitnessCreator ${tag}" \ --notes "$notes" From 89b27f9a33d418a54b7d63cda01cce562e23ca57 Mon Sep 17 00:00:00 2001 From: Nils Loose Date: Thu, 15 Oct 2026 13:34:10 +0000 Subject: [PATCH 5/5] README: library, CLI and legacy usage, migration from the Java version Co-Authored-By: Claude Opus 5.5 --- README.md | 171 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 139 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index a083f26..b842b3c 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,159 @@ # WitnessCreator -WitnessCreator creates SV-COMP violation witnesses from SWAT witness markers: +WitnessCreator creates SV-COMP violation witnesses for Java programs: - `witness.yml` in the [YAML witness format 2.3](https://gitlab.com/sosy-lab/benchmarking/sv-witnesses) - for Java (one `violation_sequence` with a `function_return` waypoint per - `Verifier.nondet*()` value and a final `target` waypoint at the violating statement), -- `witness.graphml` in the legacy GraphML format, for validators that still consume it. + (required for Java in SV-COMP 2027): one `violation_sequence` with a `function_return` + waypoint per `Verifier.nondet*()` value (`\result == ` at the closing + parenthesis of the call) and a final `target` waypoint at the violating statement; +- `witness.graphml` in the legacy GraphML format, for validators such as wit4java that + still consume it. -## Usage +It is a pure-Python package (Python >= 3.10, no runtime dependencies) with a library API, +a command line interface and a single-file `witnesscreator.pyz`. It is used by +[SWAT](https://github.com/SWAT-project/SWAT) but does not depend on it: any tool that knows +the values its `Verifier.nondet*()` calls returned and where the violation happened can use it. + +## Installation + +```sh +pip install git+https://github.com/SWAT-project/WitnessCreator@v1.0.0 +``` + +or download `witnesscreator--py3-none-any.whl` / `witnesscreator.pyz` from the +[releases](https://github.com/SWAT-project/WitnessCreator/releases). The `.pyz` needs nothing +but a Python interpreter: `python3 witnesscreator.pyz ...`. + +## Library + +```python +from witnesscreator import Task, Location, NondetReturn, Frame, Violation, write_witnesses + +task = Task( + input_files=["../common/", "Main.java"], # as given to the verifier; directories are expanded to *.java + property_file="valid-assert.prp", # its content becomes metadata.task.specification + producer_name="MyVerifier", producer_version="1.0", + base_dir=None, # resolve relative paths against this directory (default: cwd) +) + +values = [ + NondetReturn( + function="nondetInt", # Verifier method name + type="int", # boolean|byte|short|char|int|long|float|double|String + value="42", # decimal; char = code point; float/double as Java's toString + # (NaN, Infinity, -Infinity, -0.0); String = the raw string + location=Location(class_name="pkg.Main", source_file="Main.java", line=12, call_index=0), + # or Location(file_name="Main.java", line=12, column=27) when file and column are already known + ), +] + +violation = Violation( + throwable="java.lang.AssertionError", + frames=[Frame(class_name="pkg.Main", source_file="Main.java", line=17)], # innermost first +) # or None if unknown + +paths = write_witnesses(task, values, violation, out_dir="output", formats=("yaml", "graphml")) +# {'yaml': PosixPath('output/witness.yml'), 'graphml': PosixPath('output/witness.graphml')} +``` + +- `call_index` is the 0-based index of the call among the `nondet*` calls on that source + line (left to right, which is Java's evaluation order). The column of the waypoint is the + closing parenthesis of that call; calls inside comments and string literals are ignored. +- The target is the innermost stack frame that lies in the task sources (frames of `java.`, + `javax.`, `jdk.`, `sun.` and `de.uzl.its.swat.` are skipped, the package of a class must match + the file's path or `package` declaration). Its column is the `assert` of a failed assertion, + the `throw` of an explicit throw, or the first token of the statement. +- `write_witnesses` never raises for bad data: problems are logged on the `witnesscreator` + logger, each format is written independently and only the formats actually written are + returned. Without a target a warning is logged and the witness has no `target` waypoint. +- `render_yaml(task, values, violation)` and `render_graphml(...)` return the documents as strings. + +Values can also be exchanged through an event file (JSON lines, see below): +`dump_events(values, violation, producer_name, producer_version)` writes it, +`read_events(path)` reads it. + +## Command line + +`witnesscreator` (console script), `python -m witnesscreator` and `python3 witnesscreator.pyz` +are equivalent. Exit status is 0 once a witness was written, 1 if nothing could be written +and 2 for usage errors. Progress and warnings go to stderr. + +### Event file ```sh -java -jar build/libs/WitnessCreator.jar [] \ - [--input ]... [--property ] [--out ] [--producer-version ] +witnesscreator events --events run.jsonl --property valid-assert.prp \ + --input ../common/ --input Main.java --out-dir witness \ + [--format yaml|graphml|both] [--producer-name N] [--producer-version V] [--base-dir DIR] ``` -- ``: base64 of the SWAT output lines carrying `[WITNESS]` (one per nondet value: - `value@@@line@@@class@@@method@@@callIndex@@@typeDesc@@@sourceFile`) and `[WITNESS_TARGET]` - (the uncaught throwable with its stack trace) markers. An empty string yields the default - violation witness without waypoints. -- `--input`: the task's input files or directories exactly as given to the verifier. They are - searched for the sources (`.java`) and listed, with their SHA-256 hashes, in the witness. -- `--property`: the property file, its content becomes the witness' `specification`. -- `--out`: the output directory (default: ``, the legacy positional source directory). -- `--producer-version`: written to `metadata.producer.version`. +`run.jsonl` holds one JSON object per line, the serialization of the objects above: + +```json +{"kind": "header", "schema": "witnesscreator-events", "version": 1, "producer": {"name": "MyVerifier", "version": "1.0"}} +{"kind": "nondet_return", "function": "nondetInt", "type": "int", "value": "42", "location": {"class_name": "pkg.Main", "source_file": "Main.java", "line": 12, "call_index": 0}, "bits": null} +{"kind": "violation", "throwable": "java.lang.AssertionError", "frames": [{"class_name": "pkg.Main", "source_file": "Main.java", "line": 17}]} +``` -Columns are derived from the sources: a `function_return` waypoint points to the closing -parenthesis of the `callIndex`-th nondet call on its line, the `target` waypoint to the start of -the violating statement (`assert`, `throw`, or the first token of the innermost statement in the -task sources that appears in the stack trace). +The `producer` of the header is optional and overridden by `--producer-name` / +`--producer-version`. Unknown or malformed lines are skipped with a warning. -## Build +### Legacy markers (SWAT output lines) ```sh -./gradlew --no-daemon clean build +witnesscreator legacy [] [--markers-file FILE] --input ../common/ --input Main.java \ + --property valid-assert.prp --out-dir witness [--format ...] [--producer-name N] [--producer-version V] ``` -The release archive is written to `build/distributions/` and contains the -runtime layout expected by SWAT: +reads the `[WITNESS]` / `[WITNESS_TARGET]` markers SWAT printed, either as the base64 of the +output lines (as SWAT passed them to the Java tool) or from a file holding the lines (plain +text or base64). Both marker payload forms are accepted: +`value@@@line@@@class@@@methodDesc@@@callIndex@@@typeDesc@@@sourceFile` and the old +`value@@@line@@@class@@@methodDesc`. The producer defaults to `SWAT` / `unknown`. -```text -WitnessCreator/ - build/libs/WitnessCreator.jar - witnesses/default_violation.st - witnesses/witness.st +The Java tool's command line keeps working unchanged (`witnesscreator` replaces +`java -jar WitnessCreator.jar`): + +```sh +witnesscreator [] [--input PATH]... [--property FILE.prp] [--out DIR] [--producer-version V] ``` +`` is both the (only) input and the default output directory; an empty `` +yields the default violation witness without waypoints. + +## Development + +```sh +pip install -e '.[test]' +python -m pytest # unit, golden and CLI tests +WITNESSLINT="python /path/to/sv-witnesses/linter/witnesslinter.py" python -m pytest # also lint the generated witnesses +python -m build # wheel + sdist in dist/ +python tools/build_pyz.py # dist/witnesscreator.pyz +``` + +The golden tests run on copies of SV-COMP tasks in `tests/fixtures/` (see the README there); +generated witnesses are parsed with PyYAML / ElementTree and, when WitnessLint is available +(`$WITNESSLINT` or the CI setup), checked with it. + ## Release -Push a version tag such as `v0.1.0`. The CI workflow builds -`witness-creator-.zip`, uploads it as a CI artifact, and attaches it to -the GitHub release for that tag together with `SHA256SUMS`. +Push a version tag such as `v1.0.0`. The CI workflow runs the tests on Python 3.10, 3.13 +and 3.14, builds the wheel, the sdist and `witnesscreator.pyz`, and attaches them together +with `SHA256SUMS` to the GitHub release for that tag. + +## Migrating from the Java version (0.x) + +Version 1.0 replaces the Java/Gradle implementation; there is no jar and no +`witnesses/*.st` template directory any more. + +- Replace `java -jar build/libs/WitnessCreator.jar ARGS` by `witnesscreator ARGS` (or + `python3 witnesscreator.pyz ARGS`); the arguments are unchanged. The tool no longer needs to + run from a particular working directory. +- Output files and their names (`witness.yml`, `witness.graphml`) are unchanged. Differences in + content: GraphML assumptions are now `\result = ;` / `\result.equals("...");` instead + of a variable name guessed from the source line, all GraphML text is XML-escaped, and the + graph carries `sourcecodelang` and `specification`. Strings and chars escape every + non-ASCII character (`\uXXXX`) in Java literals, YAML scalars are plain ASCII. +- Target resolution is stricter: a stack frame only becomes the target when its package + matches the file (path or `package` declaration); JDK and SWAT frames are skipped. +- Progress messages are written to stderr (logging) instead of stdout. +- Prefer the library API or the event file over the markers for new integrations.