diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fa75838 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = LF +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{.phpstan/,phpstan}*.neon] +indent_style = tab + +[*.{sh,bash,zsh}] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 976068b..b831c69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,34 @@ name: CI on: - push: - pull_request: + push: ~ + pull_request: ~ jobs: + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + - run: composer install + - run: vendor/bin/phpcs + - run: vendor/bin/psalm PHPUnit: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php: - - 7.4 - - 7.3 - - 7.2 - - 7.1 - - 7.0 - - 5.6 - - 5.5 - - 5.4 - - 5.3 + - '8.3' + - '8.4' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - run: sudo apt-get -y install graphviz - run: composer install - - run: vendor/bin/phpunit --coverage-text - if: ${{ matrix.php >= 7.3 }} - - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy - if: ${{ matrix.php < 7.3 }} + - run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index de4a392..b4679ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,24 @@ +### Common +/.cache/ +/reports/ + + +### Composer /vendor /composer.lock +/graphp-graphviz-*.tar +/graphp-graphviz-*.tar.gz +/graphp-graphviz-*.tar.bz2 +/graphp-graphviz-*.zip + + +### PHPCS +/phpcs.xml + + +### PSalm +/psalm.neon + + +### PHPUnit +/phpunit.xml diff --git a/composer.json b/composer.json index 96feafc..f81df38 100644 --- a/composer.json +++ b/composer.json @@ -2,17 +2,33 @@ "name": "graphp/graphviz", "type": "library", "description": "GraphViz graph drawing for the mathematical graph/network library GraPHP.", - "keywords": ["GraphViz", "graph drawing", "graph image", "dot output", "GraPHP"], + "keywords": [ + "GraphViz", + "graph drawing", + "graph image", + "dot output", + "GraPHP" + ], "homepage": "https://github.com/graphp/graphviz", "license": "MIT", - "autoload": { - "psr-4": {"Graphp\\GraphViz\\": "src/"} - }, "require": { - "php": ">=5.3.0", + "php": ">=8.3", "graphp/graph": "^1@dev" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^11.5", + "psalm/plugin-phpunit": "^0.19.5", + "squizlabs/php_codesniffer": "^3.13", + "vimeo/psalm": "^6.13" + }, + "autoload": { + "psr-4": { + "Graphp\\GraphViz\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Graphp\\GraphViz\\Tests\\": "tests/src/" + } } } diff --git a/examples/01-simple.php b/examples/01-simple.php index 3664e5e..d762d86 100644 --- a/examples/01-simple.php +++ b/examples/01-simple.php @@ -1,8 +1,13 @@ createVertex(); $blue->setAttribute('id', 'blue'); @@ -15,5 +20,5 @@ $edge = $graph->createEdgeDirected($blue, $red); $edge->setAttribute('graphviz.color', 'grey'); -$graphviz = new Graphp\GraphViz\GraphViz(); +$graphviz = new GraphViz(); $graphviz->display($graph); diff --git a/examples/02-html.php b/examples/02-html.php index c7ea6d8..0b87229 100644 --- a/examples/02-html.php +++ b/examples/02-html.php @@ -1,25 +1,26 @@ setAttribute('graphviz.graph.rankdir', 'LR'); $hello = $graph->createVertex()->setAttribute('id', 'hello'); $world = $graph->createVertex()->setAttribute('id', 'wörld'); $graph->createEdgeDirected($hello, $world); -$graphviz = new Graphp\GraphViz\GraphViz(); +$graphviz = new GraphViz(); $graphviz->setFormat('svg'); echo ' - + hello wörld - -' . $graphviz->createImageHtml($graph) . ' - - -'; +', $graphviz->createImageHtml($graph), ''; diff --git a/examples/11-uml-html.php b/examples/11-uml-html.php index bd5011c..d00809e 100644 --- a/examples/11-uml-html.php +++ b/examples/11-uml-html.php @@ -1,10 +1,13 @@ createVertex()->setAttribute('id', 'Entity'); $a->setAttribute('graphviz.shape', 'none'); diff --git a/examples/12-uml-records.php b/examples/12-uml-records.php index 26c5f78..d9a6cdb 100644 --- a/examples/12-uml-records.php +++ b/examples/12-uml-records.php @@ -1,10 +1,13 @@ createVertex()->setAttribute('id', 'Entity'); $a->setAttribute('graphviz.shape', 'record'); diff --git a/examples/13-record-ports.php b/examples/13-record-ports.php index 790f28d..436849b 100644 --- a/examples/13-record-ports.php +++ b/examples/13-record-ports.php @@ -1,10 +1,13 @@ createVertex(); $a->setAttribute('graphviz.shape', 'Mrecord'); diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..f4fc0bb --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,12 @@ + + + + ./examples/ + ./src/ + ./tests/src/ + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7e8a89a..a63d96e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,40 @@ + + + + + ./src + + - - - - ./tests/ + + ./tests/src/Unit/ + + + + + + + + - - ./src - + + + + + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy deleted file mode 100644 index fac174b..0000000 --- a/phpunit.xml.legacy +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - ./tests/ - - - - - ./src - - - diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..292729a --- /dev/null +++ b/psalm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/Dot.php b/src/Dot.php index 0111db4..8338c98 100644 --- a/src/Dot.php +++ b/src/Dot.php @@ -1,14 +1,19 @@ graphviz = $graphviz; } - public function getOutput(Graph $graph) + public function getOutput(Graph $graph): string { return $this->graphviz->createScript($graph); } diff --git a/src/GraphViz.php b/src/GraphViz.php index fcddeb0..f0563f1 100644 --- a/src/GraphViz.php +++ b/src/GraphViz.php @@ -1,5 +1,7 @@ executable = $executable; return $this; @@ -78,20 +79,20 @@ public function setExecutable($executable) { /** * return executable to use * - * @return string * @see GraphViz::setExecutable() */ - public function getExecutable() { + public function getExecutable(): string + { return $this->executable; } /** * set graph image output format * - * @param string $format png, svg, ps2, etc. (see 'man dot' for details on parameter '-T') - * @return GraphViz $this (chainable) + * @param string $format + * png, svg, ps2, etc. (see 'man dot' for details on parameter '-T') */ - public function setFormat($format) + public function setFormat(string $format): static { $this->format = $format; @@ -101,11 +102,9 @@ public function setFormat($format) /** * create and display image for this graph * - * @param Graph $graph graph to display - * @return void * @uses GraphViz::createImageFile() */ - public function display(Graph $graph) + public function display(Graph $graph): void { // echo "Generate picture ..."; $tmp = $this->createImageFile($graph); @@ -128,34 +127,34 @@ public function display(Graph $graph) exec('xdg-open ' . escapeshellarg($tmp) . ' > /dev/null 2>&1 &'); } - $next = microtime(true) + self::DELAY_OPEN; - // echo "... done\n"; + $next = microtime(true) + (float) self::DELAY_OPEN; } /** * create image file data contents for this graph * - * @param Graph $graph graph to display - * @return string + * @param Graph $graph + * graph to display + * * @uses GraphViz::createImageFile() */ - public function createImageData(Graph $graph) + public function createImageData(Graph $graph): string { $file = $this->createImageFile($graph); $data = file_get_contents($file); unlink($file); - return $data; + return $data === false ? '' : $data; } /** * create base64-encoded image src target data to be used for html images * * @param Graph $graph graph to display - * @return string + * * @uses GraphViz::createImageData() */ - public function createImageSrc(Graph $graph) + public function createImageSrc(Graph $graph): string { $format = $this->format; if ($this->format === 'svg' || $this->format === 'svgz') { @@ -169,10 +168,10 @@ public function createImageSrc(Graph $graph) * create image html code for this graph * * @param Graph $graph graph to display - * @return string + * * @uses GraphViz::createImageSrc() */ - public function createImageHtml(Graph $graph) + public function createImageHtml(Graph $graph): string { if ($this->format === 'svg' || $this->format === 'svgz') { return ''; @@ -184,15 +183,19 @@ public function createImageHtml(Graph $graph) /** * create image file for this graph * - * @param Graph $graph graph to display - * @return string filename + * @param Graph $graph + * graph to display + * + * @return string + * filename + * * @throws \UnexpectedValueException on error + * * @uses GraphViz::createScript() */ - public function createImageFile(Graph $graph) + public function createImageFile(Graph $graph): string { $script = $this->createScript($graph); - // var_dump($script); $tmp = tempnam(sys_get_temp_dir(), 'graphviz'); if ($tmp === false) { @@ -207,26 +210,38 @@ public function createImageFile(Graph $graph) $ret = 0; $executable = $this->getExecutable(); - system(escapeshellarg($executable) . ' -T ' . escapeshellarg($this->format) . ' ' . escapeshellarg($tmp) . ' -o ' . escapeshellarg($tmp . '.' . $this->format), $ret); + $dstFilePath = $tmp . '.' . $this->format; + $command = sprintf( + "%s -T %s %s -o %s", + escapeshellcmd($executable), + escapeshellarg($this->format), + escapeshellarg($tmp), + escapeshellarg($dstFilePath), + ); + system($command, $ret); + unlink($tmp); + if ($ret !== 0) { - throw new \UnexpectedValueException('Unable to invoke "' . $executable .'" to create image file (code ' . $ret . ')'); + throw new \UnexpectedValueException(sprintf( + 'Unable to invoke "%s" to create image file (code %d)', + $executable, + $ret, + )); } - unlink($tmp); - - return $tmp . '.' . $this->format; + return $dstFilePath; } /** * create graphviz script representing this graph * * @param Graph $graph graph to display - * @return string + * @uses Directed::hasDirected() * @uses Graph::getVertices() * @uses Graph::getEdges() */ - public function createScript(Graph $graph) + public function createScript(Graph $graph): string { $hasDirectedEdges = false; foreach ($graph->getEdges() as $edge) { @@ -246,10 +261,10 @@ public function createScript(Graph $graph) $name = $this->escape($name) . ' '; } - $script = ($hasDirectedEdges ? 'di':'') . 'graph ' . $name . '{' . self::EOL; + $script = ($hasDirectedEdges ? 'di' : '') . 'graph ' . $name . '{' . self::EOL; // add global attributes - foreach (array('graph', 'node', 'edge') as $key) { + foreach (['graph', 'node', 'edge'] as $key) { if ($layout = $this->getAttributesPrefixed($graph, 'graphviz.' . $key . '.')) { $script .= $this->formatIndent . $key . ' ' . $this->escapeAttributes($layout) . self::EOL; } @@ -257,11 +272,10 @@ public function createScript(Graph $graph) // build an array to map vertex hashes to vertex IDs for output $tid = 0; - $vids = array(); + $vids = []; - $groups = array(); + $groups = []; foreach ($graph->getVertices() as $vertex) { - assert($vertex instanceof Vertex); $groups[$vertex->getAttribute('group', 0)][] = $vertex; $id = $vertex->getAttribute('id'); @@ -278,13 +292,19 @@ public function createScript(Graph $graph) $gid = 0; // put each group of vertices in a separate subgraph cluster foreach ($groups as $group => $vertices) { - $script .= $this->formatIndent . 'subgraph cluster_' . $gid++ . ' {' . self::EOL . - $indent . 'label = ' . $this->escape($group) . self::EOL; + $script .= $this->formatIndent + . 'subgraph cluster_' + . $gid++ + . ' {' + . self::EOL + . $indent . 'label = ' + . $this->escape((string) $group) + . self::EOL; foreach ($vertices as $vertex) { $vid = $vids[\spl_object_hash($vertex)]; $layout = $this->getLayoutVertex($vertex, $vid); - $script .= $indent . $this->escape($vid); + $script .= $indent . $this->escape((string) $vid); if ($layout) { $script .= ' ' . $this->escapeAttributes($layout); } @@ -295,12 +315,12 @@ public function createScript(Graph $graph) } else { // explicitly add all isolated vertices (vertices with no edges) and vertices with special layout set // other vertices wil be added automatically due to below edge definitions - foreach ($graph->getVertices() as $vertex){ + foreach ($graph->getVertices() as $vertex) { $vid = $vids[\spl_object_hash($vertex)]; $layout = $this->getLayoutVertex($vertex, $vid); if ($layout || !$vertex->getEdges()) { - $script .= $this->formatIndent . $this->escape($vid); + $script .= $this->formatIndent . $this->escape((string) $vid); if ($layout) { $script .= ' ' . $this->escapeAttributes($layout); } @@ -314,9 +334,12 @@ public function createScript(Graph $graph) // add all edges as directed edges foreach ($graph->getEdges() as $edge) { $vertices = $edge->getVertices(); - assert($vertices[0] instanceof Vertex && $vertices[1] instanceof Vertex); + assert(isset($vertices[0]) && isset($vertices[1])); - $script .= $this->formatIndent . $this->escape($vids[\spl_object_hash($vertices[0])]) . $edgeop . $this->escape($vids[\spl_object_hash($vertices[1])]); + $script .= $this->formatIndent + . $this->escape((string) $vids[\spl_object_hash($vertices[0])]) + . $edgeop + . $this->escape((string) $vids[\spl_object_hash($vertices[1])]); $layout = $this->getLayoutEdge($edge); @@ -338,11 +361,9 @@ public function createScript(Graph $graph) /** * escape given string value and wrap in quotes if needed * - * @param string $id - * @return string * @link http://graphviz.org/content/dot-language */ - private function escape($id) + private function escape(string $id): string { // see @link: There is no semantic difference between abc_2 and "abc_2" // numeric or simple string, no need to quote (only for simplicity) @@ -350,35 +371,48 @@ private function escape($id) return $id; } - return '"' . str_replace(array('&', '<', '>', '"', "'", '\\', "\n"), array('&', '<', '>', '"', ''', '\\\\', '\\l'), $id) . '"'; + $pairs = [ + '&' => '&', + '<' => '<', + '>' => '>', + '"' => '"', + "'" => ''', + '\\' => '\\\\', + "\n" => '\\l', + ]; + + return sprintf('"%s"', strtr($id, $pairs)); } /** * get escaped attribute string for given array of (unescaped) attributes * - * @param array $attrs - * @return string + * @param array $attrs + * * @uses GraphViz::escape() */ - private function escapeAttributes($attrs) + private function escapeAttributes(array $attrs): string { $script = '['; $first = true; foreach ($attrs as $name => $value) { + settype($name, 'string'); + settype($value, 'string'); + if ($first) { $first = false; } else { $script .= ' '; } - if (\substr($name, -5) === '_html') { + if (str_ends_with($name, '_html')) { // HTML-like labels need to be wrapped in angle brackets $name = \substr($name, 0, -5); $value = '<' . $value . '>'; - } elseif (\substr($name, -7) === '_record') { + } elseif (str_ends_with($name, '_record')) { // record labels need to be quoted $name = \substr($name, 0, -7); - $value = '"' . \str_replace('"', '\\"', $value) . '"'; + $value = '"' . addcslashes($value, '"') . '"'; } else { // all normal attributes need to be escaped and/or quoted $value = $this->escape($value); @@ -391,73 +425,81 @@ private function escapeAttributes($attrs) return $script; } - private function getLayoutVertex(Vertex $vertex, $vid) + /** + * @return array + */ + private function getLayoutVertex(Vertex $vertex, int|float|string $vid): array { $layout = $this->getAttributesPrefixed($vertex, 'graphviz.'); $balance = $vertex->getAttribute($this->attributeBalance); - if ($balance !== NULL) { + if ($balance !== null) { if ($balance > 0) { $balance = '+' . $balance; } if (!isset($layout['label'])) { - $layout['label'] = $vid; + $layout['label'] = (string) $vid; } - $layout['label'] .= ' (' . $balance . ')'; + $layout['label'] .= " ($balance)"; } return $layout; } - protected function getLayoutEdge(Edge $edge) + /** + * @return array + */ + protected function getLayoutEdge(Edge $edge): array { $layout = $this->getAttributesPrefixed($edge, 'graphviz.'); // use flow/capacity/weight as edge label - $label = NULL; + $label = null; $flow = $edge->getAttribute($this->attributeFlow); $capacity = $edge->getAttribute($this->attributeCapacity); // flow is set - if ($flow !== NULL) { + if ($flow !== null) { // NULL capacity = infinite capacity - $label = $flow . '/' . ($capacity === NULL ? '∞' : $capacity); + $label = $flow . '/' . ($capacity === null ? '∞' : $capacity); // capacity set, but not flow (assume zero flow) - } elseif ($capacity !== NULL) { + } elseif ($capacity !== null) { $label = '0/' . $capacity; } $weight = $edge->getAttribute($this->attributeWeight); // weight is set - if ($weight !== NULL) { - if ($label === NULL) { + if ($weight !== null) { + if ($label === null) { $label = $weight; } else { $label .= '/' . $weight; } } - if ($label !== NULL) { + if ($label !== null) { if (isset($layout['label'])) { $layout['label'] .= ' ' . $label; } else { $layout['label'] = $label; } } + return $layout; } /** - * @param Graph|Vertex|Edge $entity - * @param string $prefix - * @return array + * @param \Graphp\Graph\Graph|\Graphp\Graph\Vertex|\Graphp\Graph\Edge $entity + * @param string $prefix + * + * @return array */ - private function getAttributesPrefixed(Entity $entity, $prefix) + private function getAttributesPrefixed(Entity $entity, string $prefix): array { $len = \strlen($prefix); - $attributes = array(); + $attributes = []; foreach ($entity->getAttributes() as $name => $value) { - if (\strpos($name, $prefix) === 0) { + if (str_starts_with($name, $prefix)) { $attributes[substr($name, $len)] = $value; } } diff --git a/src/Image.php b/src/Image.php index 2d10ed2..7163477 100644 --- a/src/Image.php +++ b/src/Image.php @@ -1,14 +1,19 @@ graphviz = $graphviz; } - public function getOutput(Graph $graph) + public function getOutput(Graph $graph): string { return $this->graphviz->createImageData($graph); } @@ -26,13 +31,15 @@ public function getOutput(Graph $graph) /** * set the image output format to use * - * @param string $type png, svg - * @return self $this (chainable) + * @param string $type + * png, svg + * * @uses GraphViz::setFormat() */ - public function setFormat($type) + public function setFormat(string $type): static { $this->graphviz->setFormat($type); + return $this; } } diff --git a/tests/GraphVizTest.php b/tests/GraphVizTest.php deleted file mode 100644 index 57678de..0000000 --- a/tests/GraphVizTest.php +++ /dev/null @@ -1,424 +0,0 @@ -graphViz = new GraphViz(); - } - - public function testGraphEmpty() - { - $graph = new Graph(); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphWithName() - { - $graph = new Graph(); - $graph->setAttribute('graphviz.name', 'G'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphWithNameWithSpaces() - { - $graph = new Graph(); - $graph->setAttribute('graphviz.name', 'My Graph Name'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphIsolatedVertices() - { - $graph = new Graph(); - $graph->createVertex()->setAttribute('id', 'a'); - $graph->createVertex()->setAttribute('id', 'b'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphIsolatedVerticesWillAssignNumericIdsWhenNotExplicitlyGiven() - { - $graph = new Graph(); - $graph->createVertex(); - $graph->createVertex(); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphIsolatedVerticesWithGroupsWillBeAddedToClusters() - { - $graph = new Graph(); - $graph->createVertex()->setAttribute('id', 'a')->setAttribute('group', 0); - $graph->createVertex()->setAttribute('id', 'b')->setAttribute('group', 'foo bar')->setAttribute('graphviz.label', 'second'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphDefaultAttributes() - { - $graph = new Graph(); - $graph->setAttribute('graphviz.graph.bgcolor', 'transparent'); - $graph->setAttribute('graphviz.node.color', 'blue'); - $graph->setAttribute('graphviz.edge.color', 'grey'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testUnknownGraphAttributesWillBeDiscarded() - { - $graph = new Graph(); - $graph->setAttribute('graphviz.vertex.color', 'blue'); - $graph->setAttribute('graphviz.unknown.color', 'red'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testEscaping() - { - $graph = new Graph(); - $graph->createVertex()->setAttribute('id', 'a'); - $graph->createVertex()->setAttribute('id', 'b¹²³ is; ok\\ay, "right"?'); - $graph->createVertex()->setAttribute('id', 3); - $graph->createVertex()->setAttribute('id', 4)->setAttribute('graphviz.label', 'normal'); - $graph->createVertex()->setAttribute('id', 5)->setAttribute('graphviz.label_html', 'html-like'); - $graph->createVertex()->setAttribute('id', 6)->setAttribute('graphviz.label_html', 'hello
wörld'); - $graph->createVertex()->setAttribute('id', 7)->setAttribute('graphviz.label_record', 'first|{second1|second2}'); - $graph->createVertex()->setAttribute('id', 8)->setAttribute('graphviz.label_record', '"\N"'); - - $expected = <<html-like>] - 6 [label=wörld>] - 7 [label="first|{second1|second2}"] - 8 [label="\\"\\N\\""] -} - -VIZ; - - $this->assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphWithSimpleEdgeUsesGraphWithSimpleEdgeDefinition() - { - // a -- b - $graph = new Graph(); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', 'a'), $graph->createVertex()->setAttribute('id', 'b')); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphWithLoopUsesGraphWithSimpleLoopDefinition() - { - // a -- b -\ - // | | - // \--/ - $graph = new Graph(); - $a = $graph->createVertex()->setAttribute('id', 'a'); - $b = $graph->createVertex()->setAttribute('id', 'b'); - $graph->createEdgeUndirected($a, $b); - $graph->createEdgeUndirected($b, $b); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphDirectedUsesDigraph() - { - // a -> b - $graph = new Graph(); - $graph->createEdgeDirected($graph->createVertex()->setAttribute('id', 'a'), $graph->createVertex()->setAttribute('id', 'b')); - - $expected = << "b" -} - -VIZ; - - $this->assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphDirectedWithLoopUsesDigraphWithSimpleLoopDefinition() - { - // a -> b -\ - // ^ | - // \--/ - $graph = new Graph(); - $a = $graph->createVertex()->setAttribute('id', 'a'); - $b = $graph->createVertex()->setAttribute('id', 'b'); - $graph->createEdgeDirected($a, $b); - $graph->createEdgeDirected($b, $b); - - $expected = << "b" - "b" -> "b" -} - -VIZ; - - $this->assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphMixedUsesDigraphWithExplicitDirectionNoneForUndirectedEdges() - { - // a -> b -- c - $graph = new Graph(); - $a = $graph->createVertex()->setAttribute('id', 'a'); - $b = $graph->createVertex()->setAttribute('id', 'b'); - $c = $graph->createVertex()->setAttribute('id', 'c'); - $graph->createEdgeDirected($a, $b); - $graph->createEdgeUndirected($c, $b); - - $expected = << "b" - "c" -> "b" [dir="none"] -} - -VIZ; - - $this->assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphMixedWithDirectedLoopUsesDigraphWithoutDirectionForDirectedLoop() - { - // a -- b -\ - // ^ | - // \--/ - $graph = new Graph(); - $a = $graph->createVertex()->setAttribute('id', 'a'); - $b = $graph->createVertex()->setAttribute('id', 'b'); - $graph->createEdgeUndirected($a, $b); - $graph->createEdgeDirected($b, $b); - - $expected = << "b" [dir="none"] - "b" -> "b" -} - -VIZ; - - $this->assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testGraphUndirectedWithIsolatedVerticesFirst() - { - // a -- b -- c d - $graph = new Graph(); - $a = $graph->createVertex()->setAttribute('id', 'a'); - $b = $graph->createVertex()->setAttribute('id', 'b'); - $c = $graph->createVertex()->setAttribute('id', 'c'); - $graph->createVertex()->setAttribute('id', 'd'); - $graph->createEdgeUndirected($a, $b); - $graph->createEdgeUndirected($b, $c); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testVertexLabels() - { - $graph = new Graph(); - $graph->createVertex()->setAttribute('id', 'a')->setAttribute('balance', 1); - $graph->createVertex()->setAttribute('id', 'b')->setAttribute('balance', 0); - $graph->createVertex()->setAttribute('id', 'c')->setAttribute('balance', -1); - $graph->createVertex()->setAttribute('id', 'd')->setAttribute('graphviz.label', 'test'); - $graph->createVertex()->setAttribute('id', 'e')->setAttribute('balance', 2)->setAttribute('graphviz.label', 'unnamed'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testEdgeLayoutAtributes() - { - $graph = new Graph(); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '1a'), $graph->createVertex()->setAttribute('id', '1b')); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '2a'), $graph->createVertex()->setAttribute('id', '2b'))->setAttribute('graphviz.numeric', 20); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '3a'), $graph->createVertex()->setAttribute('id', '3b'))->setAttribute('graphviz.textual', "forty"); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '4a'), $graph->createVertex()->setAttribute('id', '4b'))->setAttribute('graphviz.1', 1)->setAttribute('graphviz.2', 2); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '5a'), $graph->createVertex()->setAttribute('id', '5b'))->setAttribute('graphviz.a', 'b')->setAttribute('graphviz.c', 'd'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testEdgeLabels() - { - $graph = new Graph(); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '1a'), $graph->createVertex()->setAttribute('id', '1b')); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '2a'), $graph->createVertex()->setAttribute('id', '2b'))->setAttribute('weight', 20); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '3a'), $graph->createVertex()->setAttribute('id', '3b'))->setAttribute('capacity', 30); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '4a'), $graph->createVertex()->setAttribute('id', '4b'))->setAttribute('flow', 40); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '5a'), $graph->createVertex()->setAttribute('id', '5b'))->setAttribute('flow', 50)->setAttribute('capacity', 60); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '6a'), $graph->createVertex()->setAttribute('id', '6b'))->setAttribute('flow', 60)->setAttribute('capacity', 70)->setAttribute('weight', 80); - $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '7a'), $graph->createVertex()->setAttribute('id', '7b'))->setAttribute('flow', 70)->setAttribute('graphviz.label', 'prefixed'); - - $expected = <<assertEquals($expected, $this->graphViz->createScript($graph)); - } - - public function testCreateImageSrcWillExportPngDefaultFormat() - { - $graph = new Graph(); - - $src = $this->graphViz->createImageSrc($graph); - - $this->assertStringStartsWith('data:image/png;base64,', $src); - } - - public function testCreateImageSrcAsSvgWithUtf8DefaultCharset() - { - $graph = new Graph(); - - $this->graphViz->setFormat('svg'); - $src = $this->graphViz->createImageSrc($graph); - - $this->assertStringStartsWith('data:image/svg+xml;charset=UTF-8;base64,', $src); - } - - public function testCreateImageSrcAsSvgzWithExplicitIsoCharsetLatin1() - { - $graph = new Graph(); - $graph->setAttribute('graphviz.graph.charset', 'iso-8859-1'); - - $this->graphViz->setFormat('svgz'); - $src = $this->graphViz->createImageSrc($graph); - - $this->assertStringStartsWith('data:image/svg+xml;charset=iso-8859-1;base64,', $src); - } -} diff --git a/tests/src/Unit/DotTest.php b/tests/src/Unit/DotTest.php new file mode 100644 index 0000000..a411325 --- /dev/null +++ b/tests/src/Unit/DotTest.php @@ -0,0 +1,40 @@ + + */ + public static function casesGetOutput(): array + { + return [ + 'empty' => [ + 'expected' => <<< 'TEXT' + graph { + } + + TEXT, + 'graph' => new Graph(), + ], + ]; + } + + #[DataProvider('casesGetOutput')] + public function testGetOutput(string $expected, Graph $graph): void + { + $dot = new Dot(); + self::assertSame($expected, $dot->getOutput($graph)); + } +} diff --git a/tests/src/Unit/GraphVizTest.php b/tests/src/Unit/GraphVizTest.php new file mode 100644 index 0000000..dc9dbf8 --- /dev/null +++ b/tests/src/Unit/GraphVizTest.php @@ -0,0 +1,556 @@ +createGraphViz(); + self::assertNotEquals('foo', $graphViz->getExecutable()); + $graphViz->setExecutable('foo'); + self::assertSame('foo', $graphViz->getExecutable()); + } + + public function testGraphEmpty(): void + { + $graph = new Graph(); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphWithName(): void + { + $graph = new Graph(); + $graph->setAttribute('graphviz.name', 'G'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphWithNameWithSpaces(): void + { + $graph = new Graph(); + $graph->setAttribute('graphviz.name', 'My Graph Name'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphIsolatedVertices(): void + { + $graph = new Graph(); + $graph->createVertex()->setAttribute('id', 'a'); + $graph->createVertex()->setAttribute('id', 'b'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphIsolatedVerticesWillAssignNumericIdsWhenNotExplicitlyGiven(): void + { + $graph = new Graph(); + $graph->createVertex(); + $graph->createVertex(); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphIsolatedVerticesWithGroupsWillBeAddedToClusters(): void + { + $graph = new Graph(); + $graph + ->createVertex() + ->setAttribute('id', 'a') + ->setAttribute('group', 0); + $graph + ->createVertex() + ->setAttribute('id', 'b') + ->setAttribute('group', 'foo bar') + ->setAttribute('graphviz.label', 'second'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphDefaultAttributes(): void + { + $graph = new Graph(); + $graph->setAttribute('graphviz.graph.bgcolor', 'transparent'); + $graph->setAttribute('graphviz.node.color', 'blue'); + $graph->setAttribute('graphviz.edge.color', 'grey'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testUnknownGraphAttributesWillBeDiscarded(): void + { + $graph = new Graph(); + $graph->setAttribute('graphviz.vertex.color', 'blue'); + $graph->setAttribute('graphviz.unknown.color', 'red'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testEscaping(): void + { + $graph = new Graph(); + $graph->createVertex()->setAttribute('id', 'a'); + $graph->createVertex()->setAttribute('id', 'b¹²³ is; ok\\ay, "right"?'); + $graph->createVertex()->setAttribute('id', 3); + $graph + ->createVertex() + ->setAttribute('id', 4) + ->setAttribute('graphviz.label', 'normal'); + $graph + ->createVertex() + ->setAttribute('id', 5) + ->setAttribute('graphviz.label_html', 'html-like'); + $graph + ->createVertex() + ->setAttribute('id', 6) + ->setAttribute('graphviz.label_html', 'hello
wörld'); + $graph + ->createVertex() + ->setAttribute('id', 7) + ->setAttribute('graphviz.label_record', 'first|{second1|second2}'); + $graph + ->createVertex() + ->setAttribute('id', 8) + ->setAttribute('graphviz.label_record', '"\N"'); + + $expected = <<html-like>] + 6 [label=wörld>] + 7 [label="first|{second1|second2}"] + 8 [label="\\"\\N\\""] + } + + VIZ; + + $graphViz = $this->createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphWithSimpleEdgeUsesGraphWithSimpleEdgeDefinition(): void + { + // a -- b + $graph = new Graph(); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', 'a'), + $graph->createVertex()->setAttribute('id', 'b'), + ); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphWithLoopUsesGraphWithSimpleLoopDefinition(): void + { + // a -- b -\ + // | | + // \--/ + $graph = new Graph(); + $a = $graph->createVertex()->setAttribute('id', 'a'); + $b = $graph->createVertex()->setAttribute('id', 'b'); + $graph->createEdgeUndirected($a, $b); + $graph->createEdgeUndirected($b, $b); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphDirectedUsesDigraph(): void + { + // a -> b + $graph = new Graph(); + $graph->createEdgeDirected( + $graph->createVertex()->setAttribute('id', 'a'), + $graph->createVertex()->setAttribute('id', 'b'), + ); + + $expected = << "b" + } + + VIZ; + + $graphViz = $this->createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphDirectedWithLoopUsesDigraphWithSimpleLoopDefinition(): void + { + // a -> b -\ + // ^ | + // \--/ + $graph = new Graph(); + $a = $graph->createVertex()->setAttribute('id', 'a'); + $b = $graph->createVertex()->setAttribute('id', 'b'); + $graph->createEdgeDirected($a, $b); + $graph->createEdgeDirected($b, $b); + + $expected = << "b" + "b" -> "b" + } + + VIZ; + + $graphViz = $this->createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphMixedUsesDigraphWithExplicitDirectionNoneForUndirectedEdges(): void + { + // a -> b -- c + $graph = new Graph(); + $a = $graph->createVertex()->setAttribute('id', 'a'); + $b = $graph->createVertex()->setAttribute('id', 'b'); + $c = $graph->createVertex()->setAttribute('id', 'c'); + $graph->createEdgeDirected($a, $b); + $graph->createEdgeUndirected($c, $b); + + $expected = << "b" + "c" -> "b" [dir="none"] + } + + VIZ; + + $graphViz = $this->createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphMixedWithDirectedLoopUsesDigraphWithoutDirectionForDirectedLoop(): void + { + // a -- b -\ + // ^ | + // \--/ + $graph = new Graph(); + $a = $graph->createVertex()->setAttribute('id', 'a'); + $b = $graph->createVertex()->setAttribute('id', 'b'); + $graph->createEdgeUndirected($a, $b); + $graph->createEdgeDirected($b, $b); + + $expected = << "b" [dir="none"] + "b" -> "b" + } + + VIZ; + + $graphViz = $this->createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testGraphUndirectedWithIsolatedVerticesFirst(): void + { + // a -- b -- c d + $graph = new Graph(); + $a = $graph->createVertex()->setAttribute('id', 'a'); + $b = $graph->createVertex()->setAttribute('id', 'b'); + $c = $graph->createVertex()->setAttribute('id', 'c'); + $graph->createVertex()->setAttribute('id', 'd'); + $graph->createEdgeUndirected($a, $b); + $graph->createEdgeUndirected($b, $c); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testVertexLabels(): void + { + $graph = new Graph(); + $graph->createVertex()->setAttribute('id', 'a')->setAttribute('balance', 1); + $graph->createVertex()->setAttribute('id', 'b')->setAttribute('balance', 0); + $graph->createVertex()->setAttribute('id', 'c')->setAttribute('balance', -1); + $graph->createVertex()->setAttribute('id', 'd')->setAttribute('graphviz.label', 'test'); + $graph + ->createVertex() + ->setAttribute('id', 'e') + ->setAttribute('balance', 2) + ->setAttribute('graphviz.label', 'unnamed'); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testEdgeLayoutAttributes(): void + { + $graph = new Graph(); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '1a'), + $graph->createVertex()->setAttribute('id', '1b'), + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '2a'), + $graph->createVertex()->setAttribute('id', '2b'), + [ + 'graphviz.numeric' => 20, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '3a'), + $graph->createVertex()->setAttribute('id', '3b'), + [ + 'graphviz.textual' => 'forty', + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '4a'), + $graph->createVertex()->setAttribute('id', '4b'), + [ + 'graphviz.1' => 1, + 'graphviz.2' => 2, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '5a'), + $graph->createVertex()->setAttribute('id', '5b'), + [ + 'graphviz.a' => 'b', + 'graphviz.c' => 'd', + ], + ); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testEdgeLabels(): void + { + $graph = new Graph(); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '1a'), + $graph->createVertex()->setAttribute('id', '1b'), + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '2a'), + $graph->createVertex()->setAttribute('id', '2b'), + [ + 'weight' => 20, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '3a'), + $graph->createVertex()->setAttribute('id', '3b'), + [ + 'capacity' => 30, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '4a'), + $graph->createVertex()->setAttribute('id', '4b'), + [ + 'flow' => 40, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '5a'), + $graph->createVertex()->setAttribute('id', '5b'), + [ + 'flow' => 50, + 'capacity' => 60, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '6a'), + $graph->createVertex()->setAttribute('id', '6b'), + [ + 'flow' => 60, + 'capacity' => 70, + 'weight' => 80, + ], + ); + $graph->createEdgeUndirected( + $graph->createVertex()->setAttribute('id', '7a'), + $graph->createVertex()->setAttribute('id', '7b'), + [ + 'flow' => 70, + 'graphviz.label' => 'prefixed', + ], + ); + + $expected = <<createGraphViz(); + self::assertSame($expected, $graphViz->createScript($graph)); + } + + public function testCreateImageSrcWillExportPngDefaultFormat(): void + { + $graph = new Graph(); + $graphViz = $this->createGraphViz(); + static::assertStringStartsWith('data:image/png;base64,', $graphViz->createImageSrc($graph)); + } + + public function testCreateImageSrcAsSvgWithUtf8DefaultCharset(): void + { + $graph = new Graph(); + $graphViz = $this->createGraphViz(); + $graphViz->setFormat('svg'); + static::assertStringStartsWith( + 'data:image/svg+xml;charset=UTF-8;base64,', + $graphViz->createImageSrc($graph), + ); + } + + public function testCreateImageSrcAsSvgzWithExplicitIsoCharsetLatin1(): void + { + $graph = new Graph(); + $graph->setAttribute('graphviz.graph.charset', 'iso-8859-1'); + $graphViz = $this->createGraphViz(); + $graphViz->setFormat('svgz'); + static::assertStringStartsWith( + 'data:image/svg+xml;charset=iso-8859-1;base64,', + $graphViz->createImageSrc($graph), + ); + } +} diff --git a/tests/src/Unit/ImageTest.php b/tests/src/Unit/ImageTest.php new file mode 100644 index 0000000..339bbe1 --- /dev/null +++ b/tests/src/Unit/ImageTest.php @@ -0,0 +1,45 @@ +getOutput($graph), + 'By default it is PNG', + ); + + $graphViz = new GraphViz(); + $graphViz->setFormat('svg'); + $image = new Image($graphViz); + self::assertStringStartsWith( + 'getOutput($graph), + 'Inherited format is used.', + ); + + $image = new Image(); + $image->setFormat('svg'); + self::assertStringStartsWith( + 'getOutput($graph), + 'Format is overridden to SVG.', + ); + } +}