/* * Copyright (C) 2019 Claus Schätzle (schaetzc@tf.uni-freiburg.de) * Copyright (C) 2019 University of Freiburg * * This file is part of the ULTIMATE Library-Sifa plug-in. * * The ULTIMATE Library-Sifa plug-in is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The ULTIMATE Library-Sifa plug-in is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the ULTIMATE Library-Sifa plug-in. If not, see . * * Additional permission under GNU GPL version 3 section 7: * If you modify the ULTIMATE Library-Sifa plug-in, or any covered work, by linking * or combining it with Eclipse RCP (or a modified version of Eclipse RCP), * containing parts covered by the terms of the Eclipse Public License, the * licensors of the ULTIMATE Library-Sifa plug-in grant you additional permission * to convey the resulting work. */ package de.uni_freiburg.informatik.ultimate.lib.sifa.test; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.Assert; import de.uni_freiburg.informatik.ultimate.lib.pathexpressions.regex.IRegex; import de.uni_freiburg.informatik.ultimate.lib.pathexpressions.regex.Regex; import de.uni_freiburg.informatik.ultimate.lib.sifa.regexdag.RegexDag; import de.uni_freiburg.informatik.ultimate.lib.sifa.regexdag.RegexDagNode; /** * Helper class to generate DAGs from human write-able strings. * DAGs can be specified in a heavily simplified version of the trivial graph format (TGF). *

* To specify a DAG its nodes and forward edges have to be listed as a space-separated list. * Each node or edge has two attributes. * Nodes have a an id (natural number) and a label (word, typically a single letter). * Edges have a source (id of the source node) and a target (id of the target node). * Both attributes are separated by a dot ".". The dot can be omitted ("1.a" = "1a" * and "1.2" = "1.2"). * In ambiguous cases the first attribute takes up as much as possible ("123" = "12.3", "12a" = "12.a"). * The source and sink of the dag are detected automatically. * * @author schaetzc@tf.uni-freiburg.de */ public class RegexDagTestUtils { private static final String SPLIT_REGEX = "\\s+"; private static final String NODE_REGEX = "(\\d+)\\.?(\\S*)"; private static final String EDGE_REGEX = "(\\d+)\\.?(\\d+)"; private static final Pattern NODE_PATTERN = Pattern.compile(NODE_REGEX); private static final Pattern EDGE_PATTERN = Pattern.compile(EDGE_REGEX); /** * Creates a DAG from a human-writable graph format. * For more information on the graph format see class documentation of {@link RegexDagTestUtils}. * @param listOfNodes Space-separated list of nodes of the form {@code 0.a} (the dot is optional). * @param listOfEdges Space-separated list of edges of the form {@code 0.1} (the dot is optional). * Backward edges are added automatically. * @return DAG */ public static RegexDag dag(final String listOfNodes, final String listOfEdges) { final Map> idToNode = new HashMap<>(); RegexDag dag = new RegexDag<>(null); for (final String nodeDescription : listOfNodes.split(SPLIT_REGEX)) { mapNewNode(nodeDescription, idToNode); } for (final String edgeDescription : listOfEdges.split(SPLIT_REGEX)) { connectNewEdge(edgeDescription, idToNode); } findAndSetSourceAndSink(idToNode.values(), dag); return dag; } private static RegexDagNode mapNewNode(final String nodeDescription, final Map> mapIdToNode) { final Matcher matcher = NODE_PATTERN.matcher(nodeDescription); if (!matcher.matches()) { throw new IllegalArgumentException("Cannot parse node: " + nodeDescription); } final String id = matcher.group(1); final String label = matcher.group(2); final RegexDagNode node = new RegexDagNode<>(stringToRegexAtom(label)); if (mapIdToNode.put(id, node) != null) { throw new IllegalStateException("Node id already used: " + id); } return node; } private static void connectNewEdge(final String edgeDescription, final Map> mapIdToNode) { final Matcher matcher = EDGE_PATTERN.matcher(edgeDescription); if (!matcher.matches()) { throw new IllegalArgumentException("Cannot parse edge: " + edgeDescription); } final Function> error = key -> { throw new IllegalArgumentException("Unknown node id: " + key); }; final RegexDagNode source = mapIdToNode.computeIfAbsent(matcher.group(1), error); final RegexDagNode target = mapIdToNode.computeIfAbsent(matcher.group(2), error); source.connectOutgoing(target); } private static void findAndSetSourceAndSink(final Collection> nodes, final RegexDag dag) { for (final RegexDagNode node : nodes) { if (node.getOutgoingNodes().isEmpty()) { if (dag.getSink() != null) { throw new IllegalArgumentException("Multiple sinks: " + dag.getSource() + " and " + node); } dag.setSink(node); } if (node.getIncomingNodes().isEmpty()) { if (dag.getSource() != null) { throw new IllegalArgumentException("Multiple sources: " + dag.getSource() + " and " + node); } dag.setSource(node); } } } public static RegexDag linearDag(final String source, final String... successors) { final RegexDag dag = RegexDag.singleNodeDag(stringToRegexAtom(source)); for (final String next : successors) { RegexDagNode nextNode = new RegexDagNode<>(stringToRegexAtom(next)); dag.getSink().connectOutgoing(nextNode); dag.setSink(nextNode); } return dag; } public static IRegex stringToRegexAtom(final String letter) { if ("".equals(letter) || "ε".equals(letter)) { return Regex.epsilon(); } else if ("∅".equals(letter)) { return Regex.emptySet(); } return Regex.literal(letter); } /** * Creates a string in trivial graph format (TGF) from a simplified version of the format. * For more information on the graph format see class documentation of {@link RegexDagTestUtils}. * @param listOfNodes Space-separated list of nodes of the form {@code 0.a} (the dot is optional). * @param listOfEdges Space-separated list of edges of the form {@code 0.1} (the dot is optional). * Backward edges are added automatically. * @return TGF */ public static String toTgf(final String listOfNodes, final String listOfEdges) { // Alternative: convert input to DAG and DAG to TGF return listOfNodes .replaceAll(SPLIT_REGEX, "\n") .replaceAll(NODE_REGEX, "$1 $2") + "\n#\n" + listOfEdges .replaceAll(SPLIT_REGEX, "\n") .replaceAll(EDGE_REGEX, "$1 $2 forward\n$2 $1 backward") + (listOfEdges.isEmpty() ? "" : "\n"); } /** * Sorts each section of a TGF file line-wise. * @param tgf String trivial graph format (TGF) * @return String in trivial graph format such that in each section the lines are sorted alphabetically */ public static String sortTgf(final String tgf) { final String[] parts = tgf.split("\n#\n"); for (int i = 0; i < parts.length; ++i) { parts[i] = sortLinewise(parts[i]); } return String.join("\n#\n", parts) + "\n"; } private static String sortLinewise(final String string) { final String[] lines = string.split("\n"); Arrays.sort(lines); return String.join("\n", lines); } /** * Constructs an expected dag from a list of nodes and edges (see {@link #toTgf(String, String)}) * and compares the expected dag with the actual dag using {@link Assert#assertEquals(String, String)}. *

* Caution: This method is very fragile. Usually we had to check graph isomorphism, which is complicated. * We compare the trivial graph format (TGF) representation which is faster but unreliable. * TGFs can differ for isomorph graph because of different node ids * A benefit of comparing TGFs is human-readable output for failed asserts. */ public static void assertEq(final String nodesExpected, final String edgesExpected, final RegexDag actualDag) { // leading \n makes jUnit's output ("expected <...> but was <...>") more readable Assert.assertEquals( "\n" + sortTgf(toTgf(nodesExpected, edgesExpected)), "\n" + sortTgf(actualDag.toString())); // TODO assert source and sink nodes are set correctly } }