Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
planarity_test.cc
Go to the documentation of this file.
1/*
2 Aleph_w
3
4 Data structures & Algorithms
5 version 2.0.0b
6 https://github.com/lrleon/Aleph-w
7
8 This file is part of Aleph-w library
9
10 Copyright (c) 2002-2026 Leandro Rabindranath Leon
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29*/
30
31
37# include <gtest/gtest.h>
38
39# include <algorithm>
40# include <cstdlib>
41# include <cstdint>
42# include <filesystem>
43# include <functional>
44# include <fstream>
45# include <limits>
46# include <optional>
47# include <random>
48# include <sstream>
49# include <string>
50# include <tuple>
51# include <unordered_map>
52# include <utility>
53# include <vector>
54
55# include <Planarity_Test.H>
56# include <tpl_agraph.H>
57# include <tpl_graph.H>
58
59using namespace Aleph;
60
61namespace
62{
66
67 using UNode = UGraph::Node;
68 using UArc = UGraph::Arc;
69
70 using Edge = std::pair<size_t, size_t>;
71
72 template <class GT>
73 struct Built_Graph
74 {
75 GT g;
76 std::vector<typename GT::Node *> nodes;
77 std::vector<typename GT::Arc *> arcs;
78 };
79
80
82 build_ugraph(const size_t n,
83 const std::vector<Edge> & edges)
84 {
86 built.nodes.reserve(n);
87 built.arcs.reserve(edges.size());
88
89 for (size_t i = 0; i < n; ++i)
90 built.nodes.push_back(built.g.insert_node(static_cast<int>(i)));
91
92 for (const auto & [u, v] : edges)
93 if (u < n and v < n)
94 built.arcs.push_back(built.g.insert_arc(built.nodes[u], built.nodes[v], 1));
95
96 return built;
97 }
98
99
101 build_dgraph(const size_t n,
102 const std::vector<Edge> & arcs)
103 {
105 built.nodes.reserve(n);
106 built.arcs.reserve(arcs.size());
107
108 for (size_t i = 0; i < n; ++i)
109 built.nodes.push_back(built.g.insert_node(static_cast<int>(i)));
110
111 for (const auto & [u, v] : arcs)
112 if (u < n and v < n)
113 built.arcs.push_back(built.g.insert_arc(built.nodes[u], built.nodes[v], 1));
114
115 return built;
116 }
117
118
120 build_array_ugraph(const size_t n,
121 const std::vector<Edge> & edges)
122 {
124 built.nodes.reserve(n);
125 built.arcs.reserve(edges.size());
126
127 for (size_t i = 0; i < n; ++i)
128 built.nodes.push_back(built.g.insert_node(static_cast<int>(i)));
129
130 for (const auto & [u, v] : edges)
131 if (u < n and v < n)
132 built.arcs.push_back(built.g.insert_arc(built.nodes[u], built.nodes[v], 1));
133
134 return built;
135 }
136
137
138 std::vector<Edge> complete_graph_edges(const size_t n)
139 {
140 std::vector<Edge> edges;
141 for (size_t u = 0; u < n; ++u)
142 for (size_t v = u + 1; v < n; ++v)
143 edges.emplace_back(u, v);
144 return edges;
145 }
146
147
148 std::vector<Edge> complete_bipartite_edges(const size_t left,
149 const size_t right)
150 {
151 std::vector<Edge> edges;
152 for (size_t u = 0; u < left; ++u)
153 for (size_t v = 0; v < right; ++v)
154 edges.emplace_back(u, left + v);
155 return edges;
156 }
157
158
159 uint64_t dart_key(const int u, const int v)
160 {
161 return (static_cast<uint64_t>(static_cast<uint32_t>(u)) << 32)
162 | static_cast<uint32_t>(v);
163 }
164
165
166 size_t component_count(const size_t n, const std::vector<Edge> & edges)
167 {
168 std::vector<std::vector<int>> adj(n);
169 for (const auto & [u, v] : edges)
170 {
171 adj[u].push_back(static_cast<int>(v));
172 adj[v].push_back(static_cast<int>(u));
173 }
174
175 std::vector<int> vis(n, 0);
176 size_t comps = 0;
177
178 for (size_t s = 0; s < n; ++s)
179 {
180 if (vis[s])
181 continue;
182
183 ++comps;
184 std::vector<int> stack = {static_cast<int>(s)};
185 vis[s] = 1;
186
187 while (not stack.empty())
188 {
189 const int v = stack.back();
190 stack.pop_back();
191 for (const int w : adj[static_cast<size_t>(v)])
192 if (not vis[static_cast<size_t>(w)])
193 {
194 vis[static_cast<size_t>(w)] = 1;
195 stack.push_back(w);
196 }
197 }
198 }
199
200 return comps;
201 }
202
203
204 std::optional<bool>
205 brute_force_planarity_oracle(const size_t n,
206 const std::vector<Edge> & edges,
207 const size_t max_combinations = 200000)
208 {
209 if (n <= 4)
210 return true;
211
212 if (edges.empty())
213 return true;
214
215 std::vector<std::vector<int>> adj(n);
216 for (const auto & [u, v] : edges)
217 {
218 adj[u].push_back(static_cast<int>(v));
219 adj[v].push_back(static_cast<int>(u));
220 }
221
222 const size_t m = edges.size();
223 const size_t c = component_count(n, edges);
224
225 // This bound is valid only for connected simple planar graphs.
226 if (c == 1 and n >= 3 and m > 3 * n - 6)
227 return false;
228
229 std::vector<std::vector<std::vector<int>>> order_options(n);
230 size_t combinations = 1;
231
232 for (size_t v = 0; v < n; ++v)
233 {
234 auto neigh = adj[v];
235 std::sort(neigh.begin(), neigh.end());
236
237 const size_t d = neigh.size();
238 if (d <= 2)
239 {
240 order_options[v].push_back(std::move(neigh));
241 continue;
242 }
243
244 const int first = neigh.front();
245 std::vector<int> tail(neigh.begin() + 1, neigh.end());
246 std::sort(tail.begin(), tail.end());
247
248 do
249 {
250 std::vector<int> order;
251 order.reserve(d);
252 order.push_back(first);
253 order.insert(order.end(), tail.begin(), tail.end());
254 order_options[v].push_back(std::move(order));
255 }
256 while (std::next_permutation(tail.begin(), tail.end()));
257
258 combinations *= order_options[v].size();
260 return std::nullopt;
261 }
262
263 std::vector<std::pair<int, int>> darts;
264 darts.reserve(2 * m);
265 std::unordered_map<uint64_t, int> dart_id;
266
267 for (const auto & [u, v] : edges)
268 {
269 const int duv = static_cast<int>(darts.size());
270 darts.emplace_back(static_cast<int>(u), static_cast<int>(v));
271 dart_id.emplace(dart_key(static_cast<int>(u), static_cast<int>(v)), duv);
272
273 const int dvu = static_cast<int>(darts.size());
274 darts.emplace_back(static_cast<int>(v), static_cast<int>(u));
275 dart_id.emplace(dart_key(static_cast<int>(v), static_cast<int>(u)), dvu);
276 }
277
278 std::vector<int> alpha(darts.size(), -1);
279 for (size_t i = 0; i < darts.size(); ++i)
280 {
281 const auto [u, v] = darts[i];
282 alpha[i] = dart_id.at(dart_key(v, u));
283 }
284
285 std::vector<size_t> selected(n, 0);
286 std::vector<size_t> vars;
287 vars.reserve(n);
288 for (size_t v = 0; v < n; ++v)
289 if (order_options[v].size() > 1)
290 vars.push_back(v);
291
292 std::sort(vars.begin(), vars.end(),
293 [&order_options](const size_t a, const size_t b)
294 {
295 return order_options[a].size() > order_options[b].size();
296 });
297
298 std::function<bool()> evaluate = [&]() -> bool
299 {
300 std::vector<int> sigma(darts.size(), -1);
301
302 for (size_t v = 0; v < n; ++v)
303 {
304 const auto & ord = order_options[v][selected[v]];
305 if (ord.empty())
306 continue;
307
308 for (size_t i = 0; i < ord.size(); ++i)
309 {
310 const int to = ord[i];
311 const int next = ord[(i + 1) % ord.size()];
312
313 const int a = dart_id.at(dart_key(static_cast<int>(v), to));
314 const int b = dart_id.at(dart_key(static_cast<int>(v), next));
315 sigma[a] = b;
316 }
317 }
318
319 for (const int s : sigma)
320 if (s < 0)
322
323 std::vector<int> vis(darts.size(), 0);
324 size_t faces = 0;
325 for (size_t d = 0; d < darts.size(); ++d)
326 {
327 if (vis[d])
328 continue;
329
330 ++faces;
331 size_t x = d;
332 while (not vis[x])
333 {
334 vis[x] = 1;
335 x = static_cast<size_t>(sigma[static_cast<size_t>(alpha[x])]);
336 }
337 }
338
339 size_t isolated_vertices = 0;
340 for (size_t v = 0; v < n; ++v)
341 if (adj[v].empty())
343 const size_t component_faces = faces + isolated_vertices;
344 const size_t global_faces = component_faces - (c == 0 ? 0 : (c - 1));
345
346 return static_cast<long long>(n)
347 - static_cast<long long>(m)
348 + static_cast<long long>(global_faces)
349 == static_cast<long long>(c) + 1;
350 };
351
352 std::function<bool(size_t)> dfs = [&](const size_t pos) -> bool
353 {
354 if (pos == vars.size())
355 return evaluate();
356
357 const size_t v = vars[pos];
358 for (size_t i = 0; i < order_options[v].size(); ++i)
359 {
360 selected[v] = i;
361 if (dfs(pos + 1))
362 return true;
363 }
364
365 return false;
366 };
367
368 return dfs(0);
369 }
370
371
372 std::string
373 shell_quote(const std::string & s)
374 {
375 std::string q = "'";
376 for (size_t i = 0; i < s.size(); ++i)
377 {
378 const char c = s[i];
379 if (c == '\'')
380 q += "'\\''";
381 else
382 q += c;
383 }
384 q += "'";
385 return q;
386 }
387
388
389 std::optional<std::filesystem::path>
391 {
392 namespace fs = std::filesystem;
393 fs::path p = fs::current_path();
394
395 for (size_t i = 0; i < 12; ++i)
396 {
397 if (fs::exists(p / "Planarity_Test.H") and fs::exists(p / "scripts"))
398 return p;
399
400 if (p == p.root_path())
401 break;
402
403 p = p.parent_path();
404 }
405
406 return std::nullopt;
407 }
408
409
410 std::string
411 make_tmp_path(const std::string & stem,
412 const std::string & ext)
413 {
414 static size_t seq = 0;
415 namespace fs = std::filesystem;
416 std::ostringstream out;
417 out << (fs::temp_directory_path() / "aleph_planarity").string()
418 << "_" << stem << "_" << ++seq << ext;
419 return out.str();
420 }
421
422
423 void
424 write_text_file(const std::string & path,
425 const std::string & content)
426 {
427 std::ofstream out(path);
428 out << content;
429 }
430
431
432 std::string
433 read_text_file(const std::string & path)
434 {
435 std::ifstream in(path);
436 std::ostringstream out;
437 out << in.rdbuf();
438 return out.str();
439 }
440
441
442 // Run a shell command, redirecting only stdout. Stderr passes through
443 // to the test output so that Ruby script errors are visible in CI logs.
444 // Clears GITHUB_OUTPUT so scripts don't try to write to the CI output
445 // file when invoked as test sub-processes.
446 int
447 run_script_cmd(const std::string & cmd_str, const std::string & label)
448 {
449 namespace fs = std::filesystem;
450 const std::string stdout_path =
451 (fs::temp_directory_path() / ("aleph_planarity_" + label + ".stdout")).string();
452 const std::string full =
453 "GITHUB_OUTPUT='' " + cmd_str + " > " + shell_quote(stdout_path);
454 return std::system(full.c_str());
455 }
456
457
458 int
459 run_external_certificate_validator(const std::vector<std::string> & inputs,
460 const bool use_networkx = false,
461 const bool require_networkx = false,
462 const bool use_gephi = false,
463 const bool require_gephi = false,
464 const std::string & gephi_cmd = "",
465 const std::string & extra_args = "",
466 const std::string & label = "validator")
467 {
468 namespace fs = std::filesystem;
469 const auto root = find_repo_root();
470 if (not root.has_value())
471 return -999;
472
473 const fs::path script = *root / "scripts" / "planarity_certificate_validator.rb";
474 if (not fs::exists(script))
475 return -998;
476
477 std::ostringstream cmd;
478 cmd << "ruby " << shell_quote(script.string());
479
480 for (size_t i = 0; i < inputs.size(); ++i)
481 cmd << " --input " << shell_quote(inputs[i]);
482
483 if (use_networkx)
484 cmd << " --networkx";
486 cmd << " --require-networkx";
487 if (use_gephi)
488 cmd << " --gephi";
489 if (require_gephi)
490 cmd << " --require-gephi";
491 if (not gephi_cmd.empty())
492 cmd << " --gephi-cmd " << shell_quote(gephi_cmd);
493 if (not extra_args.empty())
494 cmd << " " << extra_args;
495
496 return run_script_cmd(cmd.str(), label);
497 }
498
499
500 int
501 run_external_certificate_batch(const std::vector<std::string> & inputs,
502 const std::string & report_path,
503 const std::string & extra_args = "")
504 {
505 namespace fs = std::filesystem;
506 const auto root = find_repo_root();
507 if (not root.has_value())
508 return -997;
509
510 const fs::path script = *root / "scripts" / "planarity_certificate_ci_batch.rb";
511 if (not fs::exists(script))
512 return -996;
513
514 std::ostringstream cmd;
515 cmd << "ruby " << shell_quote(script.string());
516 for (size_t i = 0; i < inputs.size(); ++i)
517 cmd << " --input " << shell_quote(inputs[i]);
518 cmd << " --report " << shell_quote(report_path);
519
520 if (not extra_args.empty())
521 cmd << " " << extra_args;
522
523 return run_script_cmd(cmd.str(), "batch");
524 }
525
526
527 int
528 run_external_certificate_visual_diff(const std::vector<std::string> & inputs,
529 const std::vector<std::string> & profiles,
530 const std::string & report_path,
531 const std::string & extra_args = "")
532 {
533 namespace fs = std::filesystem;
534 const auto root = find_repo_root();
535 if (not root.has_value())
536 return -995;
537
538 const fs::path script = *root / "scripts" / "planarity_certificate_ci_visual_diff.rb";
539 if (not fs::exists(script))
540 return -994;
541
542 std::ostringstream cmd;
543 cmd << "ruby " << shell_quote(script.string());
544
545 for (size_t i = 0; i < inputs.size(); ++i)
546 cmd << " --input " << shell_quote(inputs[i]);
547
548 for (size_t i = 0; i < profiles.size(); ++i)
549 cmd << " --profile " << shell_quote(profiles[i]);
550
551 cmd << " --report " << shell_quote(report_path);
552 if (not extra_args.empty())
553 cmd << " " << extra_args;
554
555 return run_script_cmd(cmd.str(), "visual");
556 }
557
558
559 int
561 const std::string & report_json,
562 const std::string & report_md,
563 const std::string & extra_args = "")
564 {
565 namespace fs = std::filesystem;
566 const auto root = find_repo_root();
567 if (not root.has_value())
568 return -993;
569
570 const fs::path script = *root / "scripts" / "planarity_gephi_weekly_comparison.rb";
571 if (not fs::exists(script))
572 return -992;
573
574 std::ostringstream cmd;
575 cmd << "ruby " << shell_quote(script.string())
576 << " --artifacts-root " << shell_quote(artifacts_root)
577 << " --resolved-tags " << shell_quote("v0.9.7,v0.10.1")
578 << " --run-id " << shell_quote("test-run")
579 << " --run-attempt " << shell_quote("1")
580 << " --git-sha " << shell_quote("deadbeef")
581 << " --report-json " << shell_quote(report_json)
582 << " --report-md " << shell_quote(report_md);
583
584 if (not extra_args.empty())
585 cmd << " " << extra_args;
586
587 return run_script_cmd(cmd.str(), "gephi_comparison");
588 }
589
590
591 int
592 run_gephi_regression_notify(const std::string & report_json,
593 const std::string & output_md,
594 const std::string & extra_args = "")
595 {
596 namespace fs = std::filesystem;
597 const auto root = find_repo_root();
598 if (not root.has_value())
599 return -991;
600
601 const fs::path script = *root / "scripts" / "planarity_gephi_regression_notify.rb";
602 if (not fs::exists(script))
603 return -990;
604
605 std::ostringstream cmd;
606 cmd << "ruby " << shell_quote(script.string())
607 << " --report-json " << shell_quote(report_json)
608 << " --output-md " << shell_quote(output_md)
609 << " --repository " << shell_quote("lrleon/Aleph-w")
610 << " --run-url " << shell_quote("https://example.invalid/run/123");
611
612 if (not extra_args.empty())
613 cmd << " " << extra_args;
614
615 return run_script_cmd(cmd.str(), "gephi_notify");
616 }
617 // Returns the path where run_external_certificate_validator writes stdout.
618 std::string
619 validator_stdout_path(const std::string & label = "validator")
620 {
621 namespace fs = std::filesystem;
622 return (fs::temp_directory_path() / ("aleph_planarity_" + label + ".stdout")).string();
623 }
624
625
626 // Returns true when the planarity_certificate_validator.rb script can be
627 // found in the repository and Ruby is executable on this machine.
628 bool
630 {
631 namespace fs = std::filesystem;
632 const auto root = find_repo_root();
633 if (not root.has_value())
634 return false;
635 if (not fs::exists(*root / "scripts" / "planarity_certificate_validator.rb"))
636 return false;
637 return std::system("ruby --version > /dev/null 2>&1") == 0;
638 }
639
640
641 // Returns true when python3 is executable on this machine.
642 bool
644 {
645 return std::system("python3 --version > /dev/null 2>&1") == 0;
646 }
647
648} // namespace
649
650
652{
653 UGraph g;
654 const auto r = planarity_test(g);
655
656 EXPECT_TRUE(r.is_planar);
657 EXPECT_EQ(r.num_nodes, 0u);
658 EXPECT_EQ(r.simplified_num_edges, 0u);
659}
660
661
663{
664 const std::vector<Edge> edges = {
665 {0, 1}, {1, 2}, {1, 3}, {3, 4}, {3, 5}
666 };
667
668 auto built = build_ugraph(6, edges);
670}
671
672
674{
676 const auto r = planarity_test(built.g);
677
678 EXPECT_FALSE(r.is_planar);
679 EXPECT_TRUE(r.failed_euler_bound);
680}
681
682
684{
686 const auto r = planarity_test(built.g);
687
688 EXPECT_FALSE(r.is_planar);
689}
690
691
693{
695
698
699 const auto r = planarity_test(built.g, opts);
700
701 EXPECT_FALSE(r.is_planar);
702 EXPECT_TRUE(r.has_nonplanar_certificate);
703 EXPECT_EQ(r.certificate_type, Planarity_Certificate_Type::K5_Subdivision);
704 EXPECT_EQ(r.certificate_branch_nodes.size(), 5u);
705 EXPECT_EQ(r.certificate_paths.size(), 10u);
706}
707
708
710{
712
715
716 const auto r = planarity_test(built.g, opts);
717
718 EXPECT_FALSE(r.is_planar);
719 EXPECT_TRUE(r.has_nonplanar_certificate);
720 EXPECT_EQ(r.certificate_type, Planarity_Certificate_Type::K33_Subdivision);
721 EXPECT_EQ(r.certificate_branch_nodes.size(), 6u);
722 EXPECT_EQ(r.certificate_paths.size(), 9u);
723}
724
725
727{
729
730 // Add extra parallel inputs for edge {0,1}.
731 built.arcs.push_back(built.g.insert_arc(built.nodes[0], built.nodes[1], 1));
732 built.arcs.push_back(built.g.insert_arc(built.nodes[1], built.nodes[0], 1));
733
736
737 const auto r = planarity_test(built.g, opts);
738
739 ASSERT_FALSE(r.is_planar);
740 ASSERT_TRUE(r.has_nonplanar_certificate);
741 ASSERT_EQ(r.certificate_type, Planarity_Certificate_Type::K5_Subdivision);
742 ASSERT_EQ(r.certificate_obstruction_edges.size(), 10u);
743
744 bool found_01 = false;
746 it(r.certificate_obstruction_edges); it.has_curr(); it.next_ne())
747 {
748 const auto & w = it.get_curr_ne();
749 EXPECT_NE(w.representative_input_arc, nullptr);
750 EXPECT_GE(w.input_arcs.size(), 1u);
751
752 const bool is_01 = (w.src == built.nodes[0] and w.tgt == built.nodes[1])
753 or (w.src == built.nodes[1] and w.tgt == built.nodes[0]);
754 if (is_01)
755 {
756 found_01 = true;
757 EXPECT_GE(w.input_arcs.size(), 3u);
758 }
759 }
760
762}
763
764
766{
767 auto edges = complete_bipartite_edges(3, 3);
768
769 // Replace edge (0,3) by 0-6-3 to force at least one subdivided witness path.
770 auto it = std::find(edges.begin(), edges.end(), Edge{0, 3});
771 ASSERT_NE(it, edges.end());
772 edges.erase(it);
773 edges.emplace_back(0, 6);
774 edges.emplace_back(6, 3);
775
776 auto built = build_ugraph(7, edges);
777
780
781 const auto r = planarity_test(built.g, opts);
782
783 ASSERT_FALSE(r.is_planar);
784 ASSERT_TRUE(r.has_nonplanar_certificate);
785 ASSERT_EQ(r.certificate_type, Planarity_Certificate_Type::K33_Subdivision);
786 ASSERT_FALSE(r.certificate_paths.is_empty());
787
788 bool found_subdivided_path = false;
790 pit(r.certificate_paths); pit.has_curr(); pit.next_ne())
791 {
792 const auto & p = pit.get_curr_ne();
793 ASSERT_GE(p.nodes.size(), 2u);
794 EXPECT_EQ(p.edges.size(), p.nodes.size() - 1);
795
796 if (p.nodes.size() > 2)
798
800 eit(p.edges); eit.has_curr(); eit.next_ne())
801 {
802 const auto & e = eit.get_curr_ne();
803 EXPECT_NE(e.representative_input_arc, nullptr);
804 EXPECT_GE(e.input_arcs.size(), 1u);
805 }
806 }
807
809}
810
811
813{
814 auto edges = complete_graph_edges(5);
815 edges.erase(edges.begin());
816
817 auto built = build_ugraph(5, edges);
818 const auto r = planarity_test(built.g);
819
820 EXPECT_TRUE(r.is_planar);
821}
822
823
825{
826 auto edges = complete_graph_edges(5);
827 edges.erase(edges.begin()); // K5 - e, planar
828
829 auto built = build_ugraph(5, edges);
830
832 opts.compute_embedding = true;
833
834 const auto r = planarity_test(built.g, opts);
835
836 EXPECT_TRUE(r.is_planar);
837 EXPECT_TRUE(r.has_combinatorial_embedding);
838 EXPECT_FALSE(r.embedding_search_truncated);
839 EXPECT_EQ(r.embedding_rotation.size(), 5u);
840
841 const size_t expected_faces = edges.size() - 5 + 2; // connected Euler
842 EXPECT_EQ(r.embedding_num_faces, expected_faces);
843}
844
845
847{
848 auto built = build_ugraph(5, {
849 {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}, {0, 2}
850 });
851
853 opts.compute_embedding = true;
854 opts.embedding_allow_bruteforce_fallback = false;
855
856 const auto r = planarity_test(built.g, opts);
857 ASSERT_TRUE(r.is_planar);
858 ASSERT_TRUE(r.has_combinatorial_embedding);
859
860 const auto md = planar_dual_metadata(r);
861 EXPECT_TRUE(md.has_embedding);
862 EXPECT_EQ(md.num_components, 1u);
863 EXPECT_EQ(md.num_faces_local, r.embedding_num_faces);
864 EXPECT_EQ(md.num_faces_global, r.embedding_num_faces);
865 EXPECT_FALSE(md.faces_are_component_local);
866 EXPECT_EQ(md.dual_edges.size(), r.simplified_num_edges);
867 EXPECT_EQ(md.face_adjacency.size(), md.num_faces_local);
868}
869
870
872{
873 auto built = build_ugraph(4, {
874 {0, 1}, {1, 2}, {1, 3}
875 });
876
878 opts.compute_embedding = true;
879 opts.embedding_allow_bruteforce_fallback = false;
880
881 const auto r = planarity_test(built.g, opts);
882 ASSERT_TRUE(r.is_planar);
883 ASSERT_TRUE(r.has_combinatorial_embedding);
884
885 const auto md = planar_dual_metadata(r);
886 EXPECT_EQ(md.num_faces_local, 1u);
887 EXPECT_EQ(md.num_faces_global, 1u);
888 EXPECT_EQ(md.dual_edges.size(), 3u);
889
890 for (typename Array<Planar_Dual_Edge_Info<UGraph>>::Iterator
891 it(md.dual_edges); it.has_curr(); it.next_ne())
892 {
893 const auto & e = it.get_curr_ne();
894 EXPECT_EQ(e.face_a, 0u);
895 EXPECT_EQ(e.face_b, 0u);
896 }
897
899 EXPECT_EQ(dual.get_num_nodes(), 1u);
900 EXPECT_EQ(dual.get_num_arcs(), 3u);
901}
902
903
905{
906 auto built = build_ugraph(3, {
907 {0, 1}
908 });
909
911 opts.compute_embedding = true;
912 opts.embedding_allow_bruteforce_fallback = false;
913
914 const auto r = planarity_test(built.g, opts);
915 ASSERT_TRUE(r.is_planar);
916 ASSERT_TRUE(r.has_combinatorial_embedding);
917
918 const auto md = planar_dual_metadata(r);
919 EXPECT_EQ(md.num_components, 2u);
920 EXPECT_EQ(md.num_faces_local, 2u);
921 EXPECT_EQ(md.num_faces_global, 1u);
922 EXPECT_TRUE(md.faces_are_component_local);
923}
924
925
927{
928 auto built = build_ugraph(4, {
929 {0, 1}, {1, 2}, {2, 3}
930 });
931
932 const auto r = planarity_test(built.g); // no embedding requested
933 EXPECT_TRUE(r.is_planar);
934 EXPECT_FALSE(r.has_combinatorial_embedding);
935
937}
938
939
941{
942 auto built = build_ugraph(5, {
943 {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}, {0, 2}
944 });
945
947 opts.compute_embedding = true;
948 opts.embedding_allow_bruteforce_fallback = false;
949
950 const auto r = planarity_test(built.g, opts);
951
952 EXPECT_TRUE(r.is_planar);
953 EXPECT_TRUE(r.has_combinatorial_embedding);
954 EXPECT_TRUE(r.embedding_is_lr_linear);
955}
956
957
959{
960 auto edges = complete_graph_edges(5);
961 edges.erase(edges.begin()); // K5 - e, planar
962 auto built = build_ugraph(5, edges);
963
966 strict_opts.embedding_allow_bruteforce_fallback = false;
967
968 const auto r_strict = planarity_test(built.g, strict_opts);
969
970 EXPECT_TRUE(r_strict.is_planar);
971 EXPECT_TRUE(r_strict.has_combinatorial_embedding);
972 EXPECT_TRUE(r_strict.embedding_is_lr_linear);
973 EXPECT_FALSE(r_strict.embedding_search_truncated);
974
977
978 const auto r_robust = planarity_test(built.g, robust_opts);
979 EXPECT_TRUE(r_robust.is_planar);
980 EXPECT_TRUE(r_robust.has_combinatorial_embedding);
981}
982
983
985{
986 auto edges = complete_graph_edges(5);
987 edges.erase(edges.begin()); // K5 - e, planar
988 auto built = build_ugraph(5, edges);
989
991 opts.compute_embedding = true;
992 opts.embedding_allow_bruteforce_fallback = false;
993 opts.embedding_max_combinations = 1; // very low LR local-repair budget
994
995 const auto r = planarity_test(built.g, opts);
996
997 EXPECT_TRUE(r.is_planar);
998 EXPECT_FALSE(r.has_combinatorial_embedding);
999 EXPECT_TRUE(r.embedding_search_truncated);
1000}
1001
1002
1004{
1005 auto edges = complete_graph_edges(5);
1006 edges.erase(edges.begin()); // still enough rotation combinations
1007 auto built = build_ugraph(5, edges);
1008
1010 opts.compute_embedding = true;
1011 opts.embedding_prefer_lr_linear = false;
1012 opts.embedding_max_combinations = 1;
1013
1014 const auto r = planarity_test(built.g, opts);
1015
1016 EXPECT_TRUE(r.is_planar);
1017 EXPECT_FALSE(r.has_combinatorial_embedding);
1018 EXPECT_TRUE(r.embedding_search_truncated);
1019}
1020
1021
1023{
1024 auto edges = complete_bipartite_edges(3, 3);
1025 edges.erase(edges.begin());
1026
1027 auto built = build_ugraph(6, edges);
1028 const auto r = planarity_test(built.g);
1029
1030 EXPECT_TRUE(r.is_planar);
1031}
1032
1033
1035{
1036 auto edges = complete_bipartite_edges(3, 3);
1037
1038 // Replace edge (0,3) by 0-6-3
1039 auto it = std::find(edges.begin(), edges.end(), Edge{0, 3});
1040 ASSERT_NE(it, edges.end());
1041 edges.erase(it);
1042 edges.emplace_back(0, 6);
1043 edges.emplace_back(6, 3);
1044
1045 auto built = build_ugraph(7, edges);
1046 const auto r = planarity_test(built.g);
1047
1048 EXPECT_FALSE(r.is_planar);
1049}
1050
1051
1053{
1055
1058 opts.certificate_max_edges = 5; // K5 has 10 simplified edges
1059
1060 const auto r = planarity_test(built.g, opts);
1061
1062 EXPECT_FALSE(r.is_planar);
1063 EXPECT_FALSE(r.has_nonplanar_certificate);
1064 EXPECT_TRUE(r.certificate_search_truncated);
1065}
1066
1067
1069{
1070 auto built = build_ugraph(8, {
1071 {0, 1}, {0, 2}, {0, 3}, {0, 4},
1072 {1, 2}, {1, 3}, {1, 4},
1073 {2, 3}, {2, 4}, {3, 4}, // K5 on 0..4
1074 {2, 5}, {5, 6}, {6, 7} // extra planar tail
1075 });
1076
1079
1080 const auto r = planarity_test(built.g, opts);
1081
1082 EXPECT_FALSE(r.is_planar);
1083 EXPECT_TRUE(r.has_nonplanar_certificate);
1084 EXPECT_EQ(r.certificate_type, Planarity_Certificate_Type::K5_Subdivision);
1085 EXPECT_EQ(r.certificate_branch_nodes.size(), 5u);
1086 EXPECT_EQ(r.certificate_paths.size(), 10u);
1087}
1088
1089
1091{
1092 auto built = build_ugraph(9, {
1093 {0, 3}, {0, 4}, {0, 5},
1094 {1, 3}, {1, 4}, {1, 5},
1095 {2, 3}, {2, 4}, {2, 5}, // K3,3 on {0,1,2} x {3,4,5}
1096 {0, 8}, {8, 7}, {7, 6} // extra planar tail
1097 });
1098
1101
1102 const auto r = planarity_test(built.g, opts);
1103
1104 EXPECT_FALSE(r.is_planar);
1105 EXPECT_TRUE(r.has_nonplanar_certificate);
1106 EXPECT_EQ(r.certificate_type, Planarity_Certificate_Type::K33_Subdivision);
1107 EXPECT_EQ(r.certificate_branch_nodes.size(), 6u);
1108 EXPECT_EQ(r.certificate_paths.size(), 9u);
1109}
1110
1111
1113{
1115
1118 opts.certificate_max_branch_nodes_search = 4;
1119
1120 const auto r = planarity_test(built.g, opts);
1121
1122 EXPECT_FALSE(r.is_planar);
1123 EXPECT_TRUE(r.has_nonplanar_certificate);
1124 EXPECT_EQ(r.certificate_type,
1125 Planarity_Certificate_Type::Minimal_NonPlanar_Obstruction);
1126 EXPECT_TRUE(r.certificate_search_truncated);
1127}
1128
1129
1131{
1132 std::vector<Edge> edges = complete_graph_edges(5); // non-planar component
1133 edges.emplace_back(6, 7); // independent planar component
1134
1135 auto built = build_ugraph(8, edges);
1136 const auto r = planarity_test(built.g);
1137
1138 EXPECT_FALSE(r.is_planar);
1139}
1140
1141
1143{
1144 // Connected component {0,1,2,4} has n=4, m=4 and is planar.
1145 // Node 3 is isolated, so a naive global bound m <= 3n - 6c would fail here.
1146 auto built = build_ugraph(5, {
1147 {0, 2}, {1, 2}, {1, 4}, {2, 4}
1148 });
1149
1150 const auto r = planarity_test(built.g);
1151
1152 EXPECT_TRUE(r.is_planar);
1153 EXPECT_FALSE(r.failed_euler_bound);
1154}
1155
1156
1158{
1159 auto built = build_ugraph(3, {{0, 1}, {1, 2}, {2, 0}});
1160
1161 // Add loops
1162 built.arcs.push_back(built.g.insert_arc(built.nodes[0], built.nodes[0], 1));
1163 built.arcs.push_back(built.g.insert_arc(built.nodes[2], built.nodes[2], 1));
1164
1165 // Add parallel edges
1166 built.arcs.push_back(built.g.insert_arc(built.nodes[0], built.nodes[1], 1));
1167 built.arcs.push_back(built.g.insert_arc(built.nodes[1], built.nodes[0], 1));
1168
1169 const auto r = planarity_test(built.g);
1170
1171 EXPECT_TRUE(r.is_planar);
1172 EXPECT_GE(r.ignored_loops, 2u);
1173 EXPECT_GE(r.ignored_parallel_arcs, 2u);
1174}
1175
1176
1178{
1179 // Oriented K3,3
1180 auto built = build_dgraph(6, {
1181 {0, 3}, {0, 4}, {0, 5},
1182 {1, 3}, {1, 4}, {1, 5},
1183 {2, 3}, {2, 4}, {2, 5}
1184 });
1185
1186 const auto r = planarity_test(built.g);
1187 EXPECT_TRUE(r.input_is_digraph);
1188 EXPECT_FALSE(r.is_planar);
1189}
1190
1191
1193{
1194 auto planar = build_array_ugraph(5, {
1195 {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}, {0, 2}
1196 });
1198
1201}
1202
1203
1205{
1207
1208 const bool b1 = is_planar_graph(built.g);
1209 const bool b2 = Is_Planar_Graph<UGraph>()(built.g);
1210 const auto r1 = planarity_test(built.g);
1211 const auto r2 = Planarity_Test<UGraph>()(built.g);
1212
1213 EXPECT_EQ(b1, b2);
1214 EXPECT_EQ(r1.is_planar, r2.is_planar);
1215}
1216
1217
1219{
1220 std::mt19937_64 rng(0xA11CE123456789ULL);
1221 std::uniform_int_distribution<int> n_dist(1, 7);
1222 std::bernoulli_distribution has_edge(0.35);
1223
1224 size_t checked = 0;
1225 for (size_t trial = 0; trial < 3000 and checked < 70; ++trial)
1226 {
1227 const size_t n = static_cast<size_t>(n_dist(rng));
1228 std::vector<Edge> edges;
1229
1230 for (size_t u = 0; u < n; ++u)
1231 for (size_t v = u + 1; v < n; ++v)
1232 if (has_edge(rng))
1233 edges.emplace_back(u, v);
1234
1235 const auto oracle = brute_force_planarity_oracle(n, edges);
1236 if (not oracle.has_value())
1237 continue;
1238
1239 auto built = build_ugraph(n, edges);
1240 const auto got = is_planar_graph(built.g);
1241 EXPECT_EQ(got, *oracle) << "trial=" << trial;
1242 ++checked;
1243 }
1244
1245 EXPECT_GE(checked, 50u);
1246}
1247
1248
1250{
1251 std::mt19937_64 rng(0xD00D1234A11CEULL);
1252 std::uniform_int_distribution<int> n_dist(1, 7);
1253 std::bernoulli_distribution has_edge(0.36);
1254
1257 strict_opts.embedding_allow_bruteforce_fallback = false;
1260
1261 size_t checked = 0;
1262 for (size_t trial = 0; trial < 5000 and checked < 70; ++trial)
1263 {
1264 const size_t n = static_cast<size_t>(n_dist(rng));
1265 std::vector<Edge> edges;
1266
1267 for (size_t u = 0; u < n; ++u)
1268 for (size_t v = u + 1; v < n; ++v)
1269 if (has_edge(rng))
1270 edges.emplace_back(u, v);
1271
1272 const auto oracle = brute_force_planarity_oracle(n, edges);
1273 if (not oracle.has_value() or not *oracle)
1274 continue;
1275
1276 auto built = build_ugraph(n, edges);
1277 const auto r_strict = planarity_test(built.g, strict_opts);
1278 const auto r_robust = planarity_test(built.g, robust_opts);
1279
1280 SCOPED_TRACE("trial=" + std::to_string(trial)
1281 + " n=" + std::to_string(n)
1282 + " m=" + std::to_string(edges.size()));
1283
1284 EXPECT_TRUE(r_strict.is_planar);
1285 EXPECT_TRUE(r_strict.has_combinatorial_embedding);
1286 EXPECT_TRUE(r_strict.embedding_is_lr_linear);
1287 EXPECT_FALSE(r_strict.embedding_search_truncated);
1288
1289 EXPECT_TRUE(r_robust.is_planar);
1290 EXPECT_TRUE(r_robust.has_combinatorial_embedding);
1291
1292 ++checked;
1293 }
1294
1295 EXPECT_GE(checked, 50u);
1296}
1297
1298
1300{
1301 auto built = build_ugraph(3, {
1302 {0, 1}, {1, 2}, {2, 0}
1303 });
1304
1305 const auto r = planarity_test(built.g); // no embedding requested
1306 EXPECT_TRUE(r.is_planar);
1307 EXPECT_FALSE(r.has_combinatorial_embedding);
1309}
1310
1311
1313{
1314 auto built = build_ugraph(5, {
1315 {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}, {0, 2}
1316 });
1317
1319 opts.compute_embedding = true;
1320 opts.embedding_allow_bruteforce_fallback = false;
1321
1322 const auto r = planarity_test(built.g, opts);
1323 ASSERT_TRUE(r.is_planar);
1324 ASSERT_TRUE(r.has_combinatorial_embedding);
1325
1326 const auto d = planar_geometric_drawing(r);
1327 EXPECT_TRUE(d.drawing_available);
1328 EXPECT_TRUE(d.drawing_validated_no_crossings);
1329 EXPECT_EQ(d.crossing_count, 0u);
1330 EXPECT_EQ(d.node_positions.size(), r.simplified_num_nodes);
1331
1332 bool distinct = false;
1333 for (size_t i = 1; i < d.node_positions.size(); ++i)
1334 if (std::abs(d.node_positions[i].x - d.node_positions[0].x) > 1e-9
1335 or std::abs(d.node_positions[i].y - d.node_positions[0].y) > 1e-9)
1336 {
1337 distinct = true;
1338 break;
1339 }
1340 EXPECT_TRUE(distinct);
1341}
1342
1343
1345{
1346 auto edges = complete_graph_edges(5);
1347 edges.erase(edges.begin()); // K5 - e
1348 auto built = build_ugraph(5, edges);
1349
1351 opts.compute_embedding = true;
1352 opts.embedding_allow_bruteforce_fallback = false;
1353
1354 const auto r = planarity_test(built.g, opts);
1355 ASSERT_TRUE(r.is_planar);
1356 ASSERT_TRUE(r.has_combinatorial_embedding);
1357
1358 const auto d = planar_geometric_drawing(r);
1359 EXPECT_TRUE(d.drawing_available);
1360 EXPECT_TRUE(d.drawing_validated_no_crossings);
1361 EXPECT_EQ(d.crossing_count, 0u);
1362 EXPECT_NE(d.chosen_outer_face, std::numeric_limits<size_t>::max());
1363}
1364
1365
1367{
1368 auto built = build_ugraph(6, {
1369 {0, 1}, {1, 2}, {2, 0}, // component 1
1370 {3, 4}, {4, 5}, {5, 3} // component 2
1371 });
1372
1374 opts.compute_embedding = true;
1375 opts.embedding_allow_bruteforce_fallback = false;
1376 const auto r = planarity_test(built.g, opts);
1377 ASSERT_TRUE(r.is_planar);
1378 ASSERT_TRUE(r.has_combinatorial_embedding);
1379
1380 const auto d = planar_geometric_drawing(r);
1381 EXPECT_TRUE(d.drawing_available);
1382 EXPECT_TRUE(d.drawing_validated_no_crossings);
1383 EXPECT_EQ(d.num_components, 2u);
1384
1385 double min_x_a = std::numeric_limits<double>::max();
1386 double max_x_a = -std::numeric_limits<double>::max();
1387 double min_x_b = std::numeric_limits<double>::max();
1388 double max_x_b = -std::numeric_limits<double>::max();
1389
1390 for (typename Array<typename Planar_Geometric_Drawing<UGraph>::Node_Position>::Iterator
1391 it(d.node_positions); it.has_curr(); it.next_ne())
1392 {
1393 const auto & p = it.get_curr_ne();
1394 if (p.node == built.nodes[0] or p.node == built.nodes[1] or p.node == built.nodes[2])
1395 {
1396 min_x_a = std::min(min_x_a, p.x);
1397 max_x_a = std::max(max_x_a, p.x);
1398 }
1399 else
1400 {
1401 min_x_b = std::min(min_x_b, p.x);
1402 max_x_b = std::max(max_x_b, p.x);
1403 }
1404 }
1405
1406 EXPECT_GT(min_x_b - max_x_a, 0.5);
1407}
1408
1409
1411{
1412 auto built = build_ugraph(5, {
1413 {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}, {0, 2}
1414 });
1415
1417 opts.compute_embedding = true;
1418 opts.embedding_allow_bruteforce_fallback = false;
1419 const auto r = planarity_test(built.g, opts);
1420 ASSERT_TRUE(r.is_planar);
1421 ASSERT_TRUE(r.has_combinatorial_embedding);
1422
1425
1426 const auto d = planar_geometric_drawing(r, dopt);
1427 EXPECT_FALSE(d.drawing_available);
1428 EXPECT_TRUE(d.drawing_search_truncated);
1429}
1430
1431
1433{
1435
1438
1439 const auto r = planarity_test(built.g, opts);
1440 ASSERT_FALSE(r.is_planar);
1441 ASSERT_TRUE(r.has_nonplanar_certificate);
1442
1443 const auto json = nonplanar_certificate_to_json(r);
1444 const auto dot = nonplanar_certificate_to_dot(r);
1445
1446 EXPECT_NE(json.find("\"certificate_type\": \"K33_Subdivision\""), std::string::npos);
1447 EXPECT_NE(json.find("\"obstruction_edges\""), std::string::npos);
1448 EXPECT_NE(dot.find("graph NonPlanarCertificate"), std::string::npos);
1449 EXPECT_NE(dot.find("obs x"), std::string::npos);
1450}
1451
1452
1454{
1456
1459
1460 const auto r = planarity_test(built.g, opts);
1461 ASSERT_FALSE(r.is_planar);
1462 ASSERT_TRUE(r.has_nonplanar_certificate);
1463
1465 eopt.pretty_json = false;
1466 eopt.dot_highlight_paths = false;
1467
1468 const auto labeler = [](UGraph::Node *) -> std::string
1469 {
1470 return "q\"\\\n";
1471 };
1472
1473 const auto json = nonplanar_certificate_to_json(r, labeler, eopt);
1474 const auto dot = nonplanar_certificate_to_dot(r, labeler, eopt);
1475
1476 EXPECT_EQ(json.find('\n'), std::string::npos);
1477 EXPECT_NE(json.find("q\\\"\\\\\\n"), std::string::npos);
1478 EXPECT_NE(dot.find("q\\\"\\\\\\n"), std::string::npos);
1479 EXPECT_EQ(dot.find("label=\"p"), std::string::npos);
1480 EXPECT_NE(dot.find("obs x"), std::string::npos);
1481}
1482
1483
1485{
1487
1490 const auto r = planarity_test(built.g, opts);
1491 ASSERT_FALSE(r.is_planar);
1492 ASSERT_TRUE(r.has_nonplanar_certificate);
1493
1495 const auto gexf = nonplanar_certificate_to_gexf(r);
1496
1497 EXPECT_NE(graphml.find("<graphml"), std::string::npos);
1498 EXPECT_NE(graphml.find("kind"), std::string::npos);
1499 EXPECT_NE(graphml.find("obstruction"), std::string::npos);
1500
1501 EXPECT_NE(gexf.find("<gexf"), std::string::npos);
1502 EXPECT_NE(gexf.find("defaultedgetype=\"undirected\""), std::string::npos);
1503 EXPECT_NE(gexf.find("obstruction"), std::string::npos);
1504}
1505
1506
1508{
1510
1513 const auto r = planarity_test(built.g, opts);
1514 ASSERT_FALSE(r.is_planar);
1515 ASSERT_TRUE(r.has_nonplanar_certificate);
1516
1519 eopt.gexf_include_paths = false;
1520
1523
1524 EXPECT_EQ(graphml.find(">path<"), std::string::npos);
1525 EXPECT_EQ(gexf.find("value=\"path\""), std::string::npos);
1526}
1527
1528
1530{
1532
1535 const auto r = planarity_test(built.g, opts);
1536 ASSERT_FALSE(r.is_planar);
1537 ASSERT_TRUE(r.has_nonplanar_certificate);
1538
1539 const auto vr = validate_nonplanar_certificate(r);
1540 EXPECT_TRUE(vr.has_certificate);
1541 EXPECT_TRUE(vr.is_valid);
1542 EXPECT_EQ(vr.num_obstruction_edges, r.certificate_obstruction_edges.size());
1543 EXPECT_EQ(vr.num_paths, r.certificate_paths.size());
1544 EXPECT_EQ(vr.path_edge_not_in_obstruction, 0u);
1545}
1546
1547
1549{
1551
1554 auto r = planarity_test(built.g, opts);
1555 ASSERT_FALSE(r.is_planar);
1556 ASSERT_TRUE(r.has_nonplanar_certificate);
1557 ASSERT_FALSE(r.certificate_paths.is_empty());
1558 ASSERT_FALSE(r.certificate_paths[0].edges.is_empty());
1559
1560 r.certificate_paths[0].edges[0].src = nullptr;
1561
1562 const auto vr = validate_nonplanar_certificate(r);
1563 EXPECT_TRUE(vr.has_certificate);
1564 EXPECT_FALSE(vr.is_valid);
1565 EXPECT_GT(vr.null_path_edge_endpoints, 0u);
1567}
1568
1569
1571{
1573 {
1574 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1575 return;
1576 }
1577
1579
1582 const auto r = planarity_test(built.g, opts);
1583 ASSERT_FALSE(r.is_planar);
1584 ASSERT_TRUE(r.has_nonplanar_certificate);
1585
1586 const std::string graphml_path = make_tmp_path("external_valid", ".graphml");
1587 const std::string gexf_path = make_tmp_path("external_valid", ".gexf");
1588
1591
1593 EXPECT_EQ(rc, 0);
1594}
1595
1596
1598{
1600 {
1601 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1602 return;
1603 }
1604
1605 const std::string broken_graphml = make_tmp_path("external_invalid", ".graphml");
1606 write_text_file(broken_graphml, R"(<?xml version="1.0" encoding="UTF-8"?>
1607<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
1608 <graph id="Broken" edgedefault="undirected">
1609 <node id="n0"/>
1610 <node id="n1"/>
1611 <edge id="e0" source="n0" target="n1">
1612 <data key="k_edge_kind">path</data>
1613 <data key="k_edge_path_id">0</data>
1614 </edge>
1615 </graph>
1616</graphml>
1617)");
1618
1620 EXPECT_NE(rc, 0);
1621}
1622
1623
1625{
1626
1628 {
1629 GTEST_SKIP() << "Skipped: Ruby, validator script, or python3 not found";
1630 return;
1631 }
1632
1634
1637 const auto r = planarity_test(built.g, opts);
1638 ASSERT_FALSE(r.is_planar);
1639 ASSERT_TRUE(r.has_nonplanar_certificate);
1640
1641 const std::string graphml_path = make_tmp_path("external_nx", ".graphml");
1643
1646
1647 const int has_nx = std::system(
1648 "python3 -c \"import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('networkx') else 1)\"");
1650
1651 if (has_nx == 0)
1653 else
1655}
1656
1657
1659{
1661 {
1662 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1663 return;
1664 }
1665
1667
1670 const auto r = planarity_test(built.g, opts);
1671 ASSERT_FALSE(r.is_planar);
1672 ASSERT_TRUE(r.has_nonplanar_certificate);
1673
1674 const std::string graphml_path = make_tmp_path("external_gephi", ".graphml");
1676
1678 {graphml_path}, false, false, true, false);
1680
1681 const int has_gephi_usable =
1682 std::system("gephi --version > /dev/null 2>&1");
1684 {graphml_path}, false, false, true, true);
1685
1686 if (has_gephi_usable == 0)
1688 else
1690}
1691
1692
1694{
1696 {
1697 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1698 return;
1699 }
1700
1702
1705 const auto r = planarity_test(built.g, opts);
1706 ASSERT_FALSE(r.is_planar);
1707 ASSERT_TRUE(r.has_nonplanar_certificate);
1708
1709 const std::string graphml_path = make_tmp_path("external_gephi_tpl", ".graphml");
1711
1712 const std::string cmd_template =
1713 "ruby -e \"File.stat(ARGV[0])\" {input}";
1714
1716 {graphml_path}, false, false, true, true, cmd_template);
1717 EXPECT_EQ(rc, 0);
1718}
1719
1720
1722{
1723 namespace fs = std::filesystem;
1724
1726 {
1727 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1728 return;
1729 }
1730
1732
1735 const auto r = planarity_test(built.g, opts);
1736 ASSERT_FALSE(r.is_planar);
1737 ASSERT_TRUE(r.has_nonplanar_certificate);
1738
1739 const fs::path dir = fs::temp_directory_path() / "aleph planarity gephi spaces";
1740 fs::create_directories(dir);
1741 const std::string graphml_path = (dir / "k33 certificate.graphml").string();
1743
1744 const std::string cmd_template =
1745 "ruby -e \"File.stat(ARGV[0])\" {input}";
1746
1748 {graphml_path}, false, false, true, true, cmd_template);
1749 EXPECT_EQ(rc, 0);
1750}
1751
1752
1754{
1756 {
1757 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1758 return;
1759 }
1760
1761 const std::string lbl = "list_templates";
1763 {}, false, false, false, false, "", "--list-gephi-templates --json", lbl);
1764 EXPECT_EQ(rc, 0);
1765
1766 const std::string out = read_text_file(validator_stdout_path(lbl));
1767 EXPECT_NE(out.find("\"templates\""), std::string::npos);
1768 EXPECT_NE(out.find("portable.ruby-file-exists"), std::string::npos);
1769 EXPECT_NE(out.find("linux.gephi-0.10.smoke"), std::string::npos);
1770 EXPECT_NE(out.find("macos.gephi-0.10.smoke"), std::string::npos);
1771 EXPECT_NE(out.find("windows.gephi-0.10.smoke"), std::string::npos);
1772}
1773
1774
1776{
1778 {
1779 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1780 return;
1781 }
1782
1783 const std::string lbl = "filter_templates_os";
1785 {}, false, false, false, false, "",
1786 "--list-gephi-templates --template-os windows --json", lbl);
1787 EXPECT_EQ(rc, 0);
1788
1789 const std::string out = read_text_file(validator_stdout_path(lbl));
1790 EXPECT_NE(out.find("windows.gephi-0.10.smoke"), std::string::npos);
1791 EXPECT_EQ(out.find("linux.gephi-0.10.smoke"), std::string::npos);
1792}
1793
1794
1796{
1798 {
1799 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1800 return;
1801 }
1802
1803 const std::string lbl = "list_profiles";
1805 {}, false, false, false, false, "",
1806 "--list-gephi-render-profiles --json", lbl);
1807 EXPECT_EQ(rc, 0);
1808
1809 const std::string out = read_text_file(validator_stdout_path(lbl));
1810 EXPECT_NE(out.find("\"profiles\""), std::string::npos);
1811 EXPECT_NE(out.find("portable.ruby-render-svg"), std::string::npos);
1812 EXPECT_NE(out.find("linux.gephi-0.10.render-svg"), std::string::npos);
1813 EXPECT_NE(out.find("macos.gephi-0.10.render-svg"), std::string::npos);
1814 EXPECT_NE(out.find("windows.gephi-0.10.render-svg"), std::string::npos);
1815}
1816
1817
1819{
1821 {
1822 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1823 return;
1824 }
1825
1826 const std::string lbl = "filter_profiles_os";
1828 {}, false, false, false, false, "",
1829 "--list-gephi-render-profiles --render-os windows --json", lbl);
1830 EXPECT_EQ(rc, 0);
1831
1832 const std::string out = read_text_file(validator_stdout_path(lbl));
1833 EXPECT_NE(out.find("windows.gephi-0.10.render-svg"), std::string::npos);
1834 EXPECT_EQ(out.find("linux.gephi-0.10.render-svg"), std::string::npos);
1835}
1836
1837
1839{
1841 {
1842 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1843 return;
1844 }
1845
1847
1850 const auto r = planarity_test(built.g, opts);
1851 ASSERT_FALSE(r.is_planar);
1852 ASSERT_TRUE(r.has_nonplanar_certificate);
1853
1854 const std::string graphml_path = make_tmp_path("external_gephi_catalog", ".graphml");
1856
1858 {graphml_path}, false, false, true, true, "",
1859 "--gephi-template portable.ruby-file-exists");
1860 EXPECT_EQ(rc, 0);
1861}
1862
1863
1865{
1866 namespace fs = std::filesystem;
1867
1869 {
1870 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1871 return;
1872 }
1873
1875
1878 const auto r = planarity_test(built.g, opts);
1879 ASSERT_FALSE(r.is_planar);
1880 ASSERT_TRUE(r.has_nonplanar_certificate);
1881
1882 const std::string graphml_path = make_tmp_path("external_render_svg", ".graphml");
1883 const std::string output_dir = make_tmp_path("external_render_svg_dir", "");
1885
1886 const std::string lbl = "render_svg";
1888 {graphml_path}, false, false, false, false, "",
1889 "--render-gephi --require-render "
1890 "--render-profile portable.ruby-render-svg "
1891 "--render-output-dir " + shell_quote(output_dir),
1892 lbl);
1893 EXPECT_EQ(rc, 0);
1894
1895 const fs::path out_path = fs::path(output_dir) /
1896 (fs::path(graphml_path).stem().string() + ".svg");
1897 EXPECT_TRUE(fs::exists(out_path));
1898 EXPECT_GT(fs::file_size(out_path), 0u);
1899
1900 const std::string out = read_text_file(validator_stdout_path(lbl));
1901 EXPECT_NE(out.find("render_output_kind"), std::string::npos);
1902 EXPECT_NE(out.find("svg"), std::string::npos);
1903}
1904
1905
1907{
1908 namespace fs = std::filesystem;
1909
1911 {
1912 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1913 return;
1914 }
1915
1917
1920 const auto r = planarity_test(built.g, opts);
1921 ASSERT_FALSE(r.is_planar);
1922 ASSERT_TRUE(r.has_nonplanar_certificate);
1923
1924 const std::string graphml_path = make_tmp_path("external_render_pdf", ".graphml");
1925 const std::string output_dir = make_tmp_path("external_render_pdf_dir", "");
1927
1929 {graphml_path}, false, false, false, false, "",
1930 "--render-gephi --require-render "
1931 "--render-profile portable.ruby-render-pdf "
1932 "--render-output-dir " + shell_quote(output_dir));
1933 EXPECT_EQ(rc, 0);
1934
1935 const fs::path out_path = fs::path(output_dir) /
1936 (fs::path(graphml_path).stem().string() + ".pdf");
1937 EXPECT_TRUE(fs::exists(out_path));
1938 EXPECT_GT(fs::file_size(out_path), 0u);
1939}
1940
1941
1943{
1944 namespace fs = std::filesystem;
1945
1947 {
1948 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1949 return;
1950 }
1951
1953
1956 const auto r = planarity_test(built.g, opts);
1957 ASSERT_FALSE(r.is_planar);
1958 ASSERT_TRUE(r.has_nonplanar_certificate);
1959
1960 const fs::path dir = fs::temp_directory_path() / "aleph planarity render spaces";
1961 fs::create_directories(dir);
1962 const std::string graphml_path = (dir / "k33 certificate.graphml").string();
1963 const std::string output_dir = (dir / "render output").string();
1965
1967 {graphml_path}, false, false, false, false, "",
1968 "--render-gephi --require-render "
1969 "--render-profile portable.ruby-render-svg "
1970 "--render-output-dir " + shell_quote(output_dir));
1971 EXPECT_EQ(rc, 0);
1972
1973 const fs::path out_path = fs::path(output_dir) /
1974 (fs::path(graphml_path).stem().string() + ".svg");
1975 EXPECT_TRUE(fs::exists(out_path));
1976 EXPECT_GT(fs::file_size(out_path), 0u);
1977}
1978
1979
1981{
1983 {
1984 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1985 return;
1986 }
1987
1989
1992 const auto r = planarity_test(built.g, opts);
1993 ASSERT_FALSE(r.is_planar);
1994 ASSERT_TRUE(r.has_nonplanar_certificate);
1995
1996 const std::string graphml_path = make_tmp_path("external_render_bad", ".graphml");
1998
2000 {graphml_path}, false, false, false, false, "",
2001 "--render-gephi --require-render --render-profile missing.profile");
2002 EXPECT_NE(rc, 0);
2003}
2004
2005
2007{
2009
2012 const auto r = planarity_test(built.g, opts);
2013 ASSERT_FALSE(r.is_planar);
2014 ASSERT_TRUE(r.has_nonplanar_certificate);
2015
2016 const std::string graphml_path = make_tmp_path("external_batch", ".graphml");
2017 const std::string gexf_path = make_tmp_path("external_batch", ".gexf");
2018 const std::string report_path = make_tmp_path("external_batch_report", ".json");
2019
2022
2025 "--gephi --require-gephi --gephi-template portable.ruby-file-exists --print-summary");
2026 EXPECT_EQ(rc, 0);
2027
2028 const std::string report = read_text_file(report_path);
2029 EXPECT_NE(report.find("\"num_inputs\": 2"), std::string::npos);
2030 EXPECT_NE(report.find("\"overall_valid\": true"), std::string::npos);
2031 EXPECT_NE(report.find("\"gephi_template\": \"portable.ruby-file-exists\""),
2032 std::string::npos);
2033}
2034
2035
2037{
2039
2042 const auto r = planarity_test(built.g, opts);
2043 ASSERT_FALSE(r.is_planar);
2044 ASSERT_TRUE(r.has_nonplanar_certificate);
2045
2046 const std::string graphml_path = make_tmp_path("external_batch_render", ".graphml");
2047 const std::string report_path = make_tmp_path("external_batch_render_report", ".json");
2048 const std::string output_dir = make_tmp_path("external_batch_render_dir", "");
2050
2053 "--render-gephi --require-render "
2054 "--render-profile portable.ruby-render-svg "
2055 "--render-output-dir " + shell_quote(output_dir));
2056 EXPECT_EQ(rc, 0);
2057
2058 const std::string report = read_text_file(report_path);
2059 EXPECT_NE(report.find("\"render_profile\": \"portable.ruby-render-svg\""),
2060 std::string::npos);
2061 EXPECT_NE(report.find("\"overall_valid\": true"), std::string::npos);
2062}
2063
2064
2066{
2068
2071 const auto r = planarity_test(built.g, opts);
2072 ASSERT_FALSE(r.is_planar);
2073 ASSERT_TRUE(r.has_nonplanar_certificate);
2074
2075 const std::string graphml_path = make_tmp_path("external_visual", ".graphml");
2076 const std::string report_path = make_tmp_path("external_visual_report", ".json");
2077 const std::string output_dir = make_tmp_path("external_visual_render_dir", "");
2079
2081 {graphml_path},
2082 {"portable.ruby-render-svg", "portable.ruby-render-pdf"},
2084 "--render-output-dir " + shell_quote(output_dir) + " --print-summary");
2085 EXPECT_EQ(rc, 0);
2086
2087 const std::string report = read_text_file(report_path);
2088 EXPECT_NE(report.find("\"overall_passed\": true"), std::string::npos);
2089 EXPECT_NE(report.find("\"profile_id\": \"portable.ruby-render-svg\""),
2090 std::string::npos);
2091 EXPECT_NE(report.find("\"profile_id\": \"portable.ruby-render-pdf\""),
2092 std::string::npos);
2093}
2094
2095
2097{
2099
2102 const auto r = planarity_test(built.g, opts);
2103 ASSERT_FALSE(r.is_planar);
2104 ASSERT_TRUE(r.has_nonplanar_certificate);
2105
2106 const std::string graphml_path = make_tmp_path("external_visual_bad", ".graphml");
2107 const std::string report_path = make_tmp_path("external_visual_bad_report", ".json");
2108 const std::string output_dir = make_tmp_path("external_visual_bad_render_dir", "");
2109 const std::string manifest_path = make_tmp_path("external_visual_bad_manifest", ".json");
2113 R"({
2114 "schema_version": 1,
2115 "entries": [
2116 {
2117 "profile_id": "portable.ruby-render-svg",
2118 "output_ext": "svg",
2119 "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
2120 "bytes": 1
2121 }
2122 ]
2123}
2124)");
2125
2127 {graphml_path},
2128 {"portable.ruby-render-svg"},
2130 "--render-output-dir " + shell_quote(output_dir)
2131 + " --golden-manifest " + shell_quote(manifest_path));
2132 EXPECT_NE(rc, 0);
2133
2134 const std::string report = read_text_file(report_path);
2135 EXPECT_NE(report.find("\"overall_passed\": false"), std::string::npos);
2136 EXPECT_NE(report.find("Golden hash mismatch"), std::string::npos);
2137}
2138
2139
2141{
2143
2146 const auto r = planarity_test(built.g, opts);
2147 ASSERT_FALSE(r.is_planar);
2148 ASSERT_TRUE(r.has_nonplanar_certificate);
2149
2150 const std::string graphml_path = make_tmp_path("external_visual_upd", ".graphml");
2151 const std::string report_path = make_tmp_path("external_visual_upd_report", ".json");
2152 const std::string output_dir = make_tmp_path("external_visual_upd_render_dir", "");
2153 const std::string manifest_path = make_tmp_path("external_visual_upd_manifest", ".json");
2155
2157 {graphml_path},
2158 {"portable.ruby-render-svg"},
2160 "--render-output-dir " + shell_quote(output_dir)
2161 + " --golden-manifest " + shell_quote(manifest_path)
2162 + " --update-golden");
2163 EXPECT_EQ(rc, 0);
2164
2165 const std::string report = read_text_file(report_path);
2166 const std::string manifest = read_text_file(manifest_path);
2167
2168 if (manifest.find("portable.ruby-render-svg") == std::string::npos)
2169 {
2170 std::cerr << "[diag] manifest_path=" << manifest_path << "\n"
2171 << "[diag] manifest content (" << manifest.size() << " bytes):\n"
2172 << manifest << "\n"
2173 << "[diag] report content (" << report.size() << " bytes):\n"
2174 << report << "\n";
2175 }
2176
2177 EXPECT_NE(manifest.find("\"profile_id\": \"portable.ruby-render-svg\""),
2178 std::string::npos);
2179 EXPECT_NE(manifest.find("0459f595d6268e7bdf9e8e273d4394fa50d23a89ec3d2449d10b7b47941b1327"),
2180 std::string::npos);
2181}
2182
2183
2185{
2186 namespace fs = std::filesystem;
2187
2188 const fs::path artifacts_root =
2189 fs::path(make_tmp_path("gephi_nightly_artifacts_ok", ""));
2190 fs::create_directories(artifacts_root);
2191
2192 const auto write_case = [&](const std::string & dir_name,
2193 const std::string & os_name,
2194 const std::string & tag,
2195 const int validator_exit_code,
2196 const bool overall_valid)
2197 {
2198 const fs::path dir = artifacts_root / dir_name;
2199 fs::create_directories(dir);
2200
2201 write_text_file((dir / "downloaded_gephi_asset.txt").string(),
2202 "tag=" + tag + "\n"
2203 "runner_os=" + os_name + "\n"
2204 "asset_name=gephi-package-" + tag + ".zip\n"
2205 "gephi_executable=/tmp/gephi\n");
2206
2207 std::ostringstream summary;
2208 summary << "{\n"
2209 << " \"os\": \"" << os_name << "\",\n"
2210 << " \"gephi_tag\": \"" << tag << "\",\n"
2211 << " \"asset_name\": \"gephi-package-" << tag << ".zip\",\n"
2212 << " \"validator_exit_code\": " << validator_exit_code << ",\n"
2213 << " \"overall_valid\": " << (overall_valid ? "true" : "false") << "\n"
2214 << "}\n";
2215 write_text_file((dir / "nightly_case_summary.json").string(), summary.str());
2216 };
2217
2218 write_case("gephi-nightly-ubuntu-24.04-v0.9.7", "ubuntu-24.04", "v0.9.7", 0, true);
2219 write_case("gephi-nightly-ubuntu-24.04-v0.10.1", "ubuntu-24.04", "v0.10.1", 0, true);
2220 write_case("gephi-nightly-windows-2022-v0.9.7", "windows-2022", "v0.9.7", 0, true);
2221 write_case("gephi-nightly-windows-2022-v0.10.1", "windows-2022", "v0.10.1", 0, true);
2222
2223 const std::string report_json = make_tmp_path("gephi_nightly_report_ok", ".json");
2224 const std::string report_md = make_tmp_path("gephi_nightly_report_ok", ".md");
2225
2226 const int rc = run_gephi_nightly_comparison(
2227 artifacts_root.string(), report_json, report_md, "--print-summary");
2228 EXPECT_EQ(rc, 0);
2229
2230 const std::string json = read_text_file(report_json);
2231 EXPECT_NE(json.find("\"num_entries\": 4"), std::string::npos);
2232 EXPECT_NE(json.find("\"num_regressions\": 0"), std::string::npos);
2233 EXPECT_NE(json.find("\"has_regressions\": false"), std::string::npos);
2234
2235 const std::string markdown = read_text_file(report_md);
2236 EXPECT_NE(markdown.find("### Regression Alerts"), std::string::npos);
2237 EXPECT_NE(markdown.find("- none"), std::string::npos);
2238}
2239
2240
2242{
2243 namespace fs = std::filesystem;
2244
2245 const fs::path artifacts_root =
2246 fs::path(make_tmp_path("gephi_nightly_artifacts_bad", ""));
2247 fs::create_directories(artifacts_root);
2248
2249 const auto write_case = [&](const std::string & dir_name,
2250 const std::string & os_name,
2251 const std::string & tag,
2252 const int validator_exit_code,
2253 const bool overall_valid)
2254 {
2255 const fs::path dir = artifacts_root / dir_name;
2256 fs::create_directories(dir);
2257
2258 write_text_file((dir / "downloaded_gephi_asset.txt").string(),
2259 "tag=" + tag + "\n"
2260 "runner_os=" + os_name + "\n"
2261 "asset_name=gephi-package-" + tag + ".zip\n"
2262 "gephi_executable=/tmp/gephi\n");
2263
2264 std::ostringstream summary;
2265 summary << "{\n"
2266 << " \"os\": \"" << os_name << "\",\n"
2267 << " \"gephi_tag\": \"" << tag << "\",\n"
2268 << " \"asset_name\": \"gephi-package-" << tag << ".zip\",\n"
2269 << " \"validator_exit_code\": " << validator_exit_code << ",\n"
2270 << " \"overall_valid\": " << (overall_valid ? "true" : "false") << "\n"
2271 << "}\n";
2272 write_text_file((dir / "nightly_case_summary.json").string(), summary.str());
2273 };
2274
2275 write_case("gephi-nightly-ubuntu-24.04-v0.9.7", "ubuntu-24.04", "v0.9.7", 0, true);
2276 write_case("gephi-nightly-ubuntu-24.04-v0.10.1", "ubuntu-24.04", "v0.10.1", 2, false);
2277
2278 const std::string report_json = make_tmp_path("gephi_nightly_report_bad", ".json");
2279 const std::string report_md = make_tmp_path("gephi_nightly_report_bad", ".md");
2280
2281 const int rc = run_gephi_nightly_comparison(
2282 artifacts_root.string(), report_json, report_md, "--print-summary");
2283 EXPECT_EQ(rc, 0);
2284
2285 const std::string json = read_text_file(report_json);
2286 EXPECT_NE(json.find("\"num_regressions\": 1"), std::string::npos);
2287 EXPECT_NE(json.find("\"has_regressions\": true"), std::string::npos);
2288 EXPECT_NE(json.find("overall_valid_regressed"), std::string::npos);
2289 EXPECT_NE(json.find("exit_code_regressed"), std::string::npos);
2290
2292 artifacts_root.string(), report_json, report_md, "--fail-on-regressions");
2293 EXPECT_NE(rc_fail, 0);
2294}
2295
2296
2298{
2299 const std::string report_json = make_tmp_path("gephi_notify_ok_report", ".json");
2300 const std::string output_md = make_tmp_path("gephi_notify_ok_alert", ".md");
2301
2304 R"({
2305 "schema_version": 1,
2306 "run_id": "r1",
2307 "run_attempt": "1",
2308 "git_sha": "abc123",
2309 "resolved_tags": "v0.9.7,v0.10.1",
2310 "num_entries": 4,
2311 "entries": [],
2312 "num_regressions": 0,
2313 "has_regressions": false,
2314 "regressions": []
2315}
2316)");
2317
2318 const int rc = run_gephi_regression_notify(report_json, output_md, "--print-summary");
2319 EXPECT_EQ(rc, 0);
2320
2321 const std::string markdown = read_text_file(output_md);
2322 EXPECT_NE(markdown.find("Gephi Nightly Regression Alert"), std::string::npos);
2323 EXPECT_NE(markdown.find("No regressions detected."), std::string::npos);
2324}
2325
2326
2328{
2329 const std::string report_json = make_tmp_path("gephi_notify_bad_report", ".json");
2330 const std::string output_md = make_tmp_path("gephi_notify_bad_alert", ".md");
2331
2334 R"({
2335 "schema_version": 1,
2336 "run_id": "r2",
2337 "run_attempt": "1",
2338 "git_sha": "def456",
2339 "resolved_tags": "v0.9.7,v0.10.1",
2340 "num_entries": 2,
2341 "entries": [],
2342 "num_regressions": 1,
2343 "has_regressions": true,
2344 "regressions": [
2345 {
2346 "os": "ubuntu-24.04",
2347 "from_tag": "v0.9.7",
2348 "to_tag": "v0.10.1",
2349 "from_overall_valid": true,
2350 "to_overall_valid": false,
2351 "from_exit_code": 0,
2352 "to_exit_code": 2,
2353 "reason_codes": ["overall_valid_regressed", "exit_code_regressed"]
2354 }
2356}
2357)");
2358
2360 report_json, output_md, "--print-summary");
2362
2363 const std::string markdown = read_text_file(output_md);
2364 EXPECT_NE(markdown.find("Detected regression(s):"), std::string::npos);
2365 EXPECT_NE(markdown.find("overall_valid_regressed"), std::string::npos);
2366 EXPECT_NE(markdown.find("exit_code_regressed"), std::string::npos);
2367 EXPECT_NE(markdown.find("https://example.invalid/run/123"), std::string::npos);
2368
2370 report_json, output_md, "--require-webhook-success");
2372}
2373
2374
2376{
2377 const std::string broken_graphml = make_tmp_path("external_batch_invalid", ".graphml");
2378 const std::string report_path = make_tmp_path("external_batch_invalid_report", ".json");
2379
2380 write_text_file(broken_graphml, R"(<?xml version="1.0" encoding="UTF-8"?>
2381<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
2382 <graph id="Broken" edgedefault="undirected">
2383 <node id="n0"/>
2384 <node id="n1"/>
2385 <edge id="e0" source="n0" target="n1">
2386 <data key="k_edge_kind">path</data>
2387 </edge>
2388 </graph>
2389</graphml>
2390)");
2391
2393 EXPECT_NE(rc, 0);
2394
2395 const std::string report = read_text_file(report_path);
2396 EXPECT_NE(report.find("\"num_inputs\": 1"), std::string::npos);
2397 EXPECT_NE(report.find("\"overall_valid\": false"), std::string::npos);
2398}
2399
2400
2402{
2403 namespace fs = std::filesystem;
2404
2406 {
2407 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
2408 return;
2409 }
2410
2411 const auto root = find_repo_root();
2412 ASSERT_TRUE(root.has_value());
2413
2414 const fs::path fixture =
2415 *root / "scripts" / "fixtures" / "planarity_k33_certificate.graphml";
2416 ASSERT_TRUE(fs::exists(fixture));
2417
2418 const int rc = run_external_certificate_validator({fixture.string()});
2419 EXPECT_EQ(rc, 0);
2420}
2421
2422
2424{
2426
2427 const auto r = planarity_test(built.g); // no certificate requested
2428 ASSERT_FALSE(r.is_planar);
2429 ASSERT_FALSE(r.has_nonplanar_certificate);
2430
2435
2436 const auto vr = validate_nonplanar_certificate(r);
2437 EXPECT_FALSE(vr.has_certificate);
2438 EXPECT_FALSE(vr.is_valid);
2440}
Planarity testing on Aleph graphs.
long double vr
Definition btreepic.C:150
long double w
Definition btreepic.C:153
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:139
Generic directed graph (digraph) wrapper template.
Definition graph-dry.H:3854
Graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:428
_Graph_Node Node
The graph type.
Definition tpl_graph.H:432
_Graph_Arc Arc
The node class type.
Definition tpl_graph.H:433
constexpr size_t size() const noexcept
Returns the number of entries in the table.
Definition hashDry.H:619
#define TEST(name)
static mt19937 rng
__gmp_expr< T, __gmp_binary_expr< __gmp_expr< T, U >, unsigned long int, __gmp_root_function > > root(const __gmp_expr< T, U > &expr, unsigned long int l)
Definition gmpfrxx.h:4060
DynArray< Graph::Node * > nodes
Definition graphpic.C:406
DynArray< Graph::Arc * > arcs
Definition graphpic.C:408
std::string nonplanar_certificate_to_gexf(const Planarity_Test_Result< GT > &result, Node_Label node_label=Node_Label(), const NonPlanar_Certificate_Export_Options &options=NonPlanar_Certificate_Export_Options())
Export non-planar certificate as GEXF.
Planar_Geometric_Drawing< GT > planar_geometric_drawing(const Planarity_Test_Result< GT > &result, const Planar_Geometric_Drawing_Options &options=Planar_Geometric_Drawing_Options())
Build embedding-aware 2D node coordinates from a planar result.
Planarity_Test_Result< GT > planarity_test(const GT &g, SA sa=SA(), const Planarity_Test_Options &options=Planarity_Test_Options())
Execute planarity test on an Aleph graph.
Planar_Dual_Metadata< GT > planar_dual_metadata(const Planarity_Test_Result< GT > &result)
Build face and dual-edge metadata from a planar embedding result.
std::string nonplanar_certificate_to_dot(const Planarity_Test_Result< GT > &result, Node_Label node_label=Node_Label(), const NonPlanar_Certificate_Export_Options &options=NonPlanar_Certificate_Export_Options())
Export non-planar certificate as GraphViz DOT.
bool is_planar_graph(const GT &g, SA sa=SA(), const Planarity_Test_Options &options=Planarity_Test_Options())
Convenience boolean API for planarity.
std::string nonplanar_certificate_to_graphml(const Planarity_Test_Result< GT > &result, Node_Label node_label=Node_Label(), const NonPlanar_Certificate_Export_Options &options=NonPlanar_Certificate_Export_Options())
Export non-planar certificate as GraphML.
std::string nonplanar_certificate_to_json(const Planarity_Test_Result< GT > &result, Node_Label node_label=Node_Label(), const NonPlanar_Certificate_Export_Options &options=NonPlanar_Certificate_Export_Options())
Export non-planar certificate as JSON.
NonPlanar_Certificate_Validation< GT > validate_nonplanar_certificate(const Planarity_Test_Result< GT > &result)
Validate structural consistency of a non-planar certificate.
bool nonplanar_certificate_is_valid(const Planarity_Test_Result< GT > &result)
Convenience predicate over validate_nonplanar_certificate().
DFSResult< typename Domain::Distance > dfs(Domain &domain, typename Domain::State &state, SearchPath< typename Domain::Move > &path, const typename Domain::Distance g, const typename Domain::Distance threshold, const size_t depth, IDAStarResult< Solution, typename Domain::Distance > &result, OnSolution &on_solution)
Core recursive DFS used by IDA* for a single threshold pass.
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
and
Check uniqueness with explicit hash + equality functors.
size_t size(Node *root) noexcept
Divide_Conquer_DP_Result< Cost > divide_and_conquer_partition_dp(const size_t groups, const size_t n, Transition_Cost_Fn transition_cost, const Cost inf=dp_optimization_detail::default_inf< Cost >())
Optimize partition DP using divide-and-conquer optimization.
void next()
Advance all underlying iterators (bounds-checked).
Definition ah-zip.H:171
Default node labeler for certificate export.
Arc of graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:222
Export options for non-planar certificate serializers.
bool pretty_json
Pretty-print JSON with indentation/newlines.
bool graphml_include_paths
Include path overlays in GraphML output.
Dual-edge payload linked to a primal simplified edge.
Options for embedding-aware 2D drawing extraction.
size_t max_outer_faces_to_try
Max outer-face candidates evaluated per component.
Configuration for planarity test optional outputs.
bool embedding_allow_bruteforce_fallback
Allow exact embedding fallback if LR-constructive embedding fails validation.
bool compute_embedding
If true and graph is planar, tries to compute combinatorial embedding.
bool compute_nonplanar_certificate
If true and graph is non-planar, tries to extract witness.
FooMap m(5, fst_unit_pair_hash, snd_unit_pair_hash)
CmdLine cmd
Definition testHash.C:43
gsl_rng * r
Array-based graph implementation.
Generic graph and digraph implementations.