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 std::string lbl = "gephi_portable_mode";
1683 {graphml_path}, false, false, true, true, "", "", lbl);
1685
1686 const std::string report = read_text_file(validator_stdout_path(lbl));
1687 EXPECT_NE(report.find("overall_valid=true"), std::string::npos);
1688 EXPECT_NE(report.find("running smoke check only"), std::string::npos);
1689}
1690
1691
1693{
1695 {
1696 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1697 return;
1698 }
1699
1701
1704 const auto r = planarity_test(built.g, opts);
1705 ASSERT_FALSE(r.is_planar);
1706 ASSERT_TRUE(r.has_nonplanar_certificate);
1707
1708 const std::string graphml_path = make_tmp_path("external_gephi_tpl", ".graphml");
1710
1711 const std::string cmd_template =
1712 "ruby -e \"File.stat(ARGV[0])\" {input}";
1713
1715 {graphml_path}, false, false, true, true, cmd_template);
1716 EXPECT_EQ(rc, 0);
1717}
1718
1719
1721{
1722 namespace fs = std::filesystem;
1723
1725 {
1726 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1727 return;
1728 }
1729
1731
1734 const auto r = planarity_test(built.g, opts);
1735 ASSERT_FALSE(r.is_planar);
1736 ASSERT_TRUE(r.has_nonplanar_certificate);
1737
1738 const fs::path dir = fs::temp_directory_path() / "aleph planarity gephi spaces";
1739 fs::create_directories(dir);
1740 const std::string graphml_path = (dir / "k33 certificate.graphml").string();
1742
1743 const std::string cmd_template =
1744 "ruby -e \"File.stat(ARGV[0])\" {input}";
1745
1747 {graphml_path}, false, false, true, true, cmd_template);
1748 EXPECT_EQ(rc, 0);
1749}
1750
1751
1753{
1755 {
1756 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1757 return;
1758 }
1759
1760 const std::string lbl = "list_templates";
1762 {}, false, false, false, false, "", "--list-gephi-templates --json", lbl);
1763 EXPECT_EQ(rc, 0);
1764
1765 const std::string out = read_text_file(validator_stdout_path(lbl));
1766 EXPECT_NE(out.find("\"templates\""), std::string::npos);
1767 EXPECT_NE(out.find("portable.ruby-file-exists"), std::string::npos);
1768 EXPECT_NE(out.find("linux.gephi-0.10.smoke"), std::string::npos);
1769 EXPECT_NE(out.find("macos.gephi-0.10.smoke"), std::string::npos);
1770 EXPECT_NE(out.find("windows.gephi-0.10.smoke"), std::string::npos);
1771}
1772
1773
1775{
1777 {
1778 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1779 return;
1780 }
1781
1782 const std::string lbl = "filter_templates_os";
1784 {}, false, false, false, false, "",
1785 "--list-gephi-templates --template-os windows --json", lbl);
1786 EXPECT_EQ(rc, 0);
1787
1788 const std::string out = read_text_file(validator_stdout_path(lbl));
1789 EXPECT_NE(out.find("windows.gephi-0.10.smoke"), std::string::npos);
1790 EXPECT_EQ(out.find("linux.gephi-0.10.smoke"), std::string::npos);
1791}
1792
1793
1795{
1797 {
1798 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1799 return;
1800 }
1801
1802 const std::string lbl = "list_profiles";
1804 {}, false, false, false, false, "",
1805 "--list-gephi-render-profiles --json", lbl);
1806 EXPECT_EQ(rc, 0);
1807
1808 const std::string out = read_text_file(validator_stdout_path(lbl));
1809 EXPECT_NE(out.find("\"profiles\""), std::string::npos);
1810 EXPECT_NE(out.find("portable.ruby-render-svg"), std::string::npos);
1811 EXPECT_NE(out.find("linux.gephi-0.10.render-svg"), std::string::npos);
1812 EXPECT_NE(out.find("macos.gephi-0.10.render-svg"), std::string::npos);
1813 EXPECT_NE(out.find("windows.gephi-0.10.render-svg"), std::string::npos);
1814}
1815
1816
1818{
1820 {
1821 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1822 return;
1823 }
1824
1825 const std::string lbl = "filter_profiles_os";
1827 {}, false, false, false, false, "",
1828 "--list-gephi-render-profiles --render-os windows --json", lbl);
1829 EXPECT_EQ(rc, 0);
1830
1831 const std::string out = read_text_file(validator_stdout_path(lbl));
1832 EXPECT_NE(out.find("windows.gephi-0.10.render-svg"), std::string::npos);
1833 EXPECT_EQ(out.find("linux.gephi-0.10.render-svg"), std::string::npos);
1834}
1835
1836
1838{
1840 {
1841 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1842 return;
1843 }
1844
1846
1849 const auto r = planarity_test(built.g, opts);
1850 ASSERT_FALSE(r.is_planar);
1851 ASSERT_TRUE(r.has_nonplanar_certificate);
1852
1853 const std::string graphml_path = make_tmp_path("external_gephi_catalog", ".graphml");
1855
1857 {graphml_path}, false, false, true, true, "",
1858 "--gephi-template portable.ruby-file-exists");
1859 EXPECT_EQ(rc, 0);
1860}
1861
1862
1864{
1865 namespace fs = std::filesystem;
1866
1868 {
1869 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1870 return;
1871 }
1872
1874
1877 const auto r = planarity_test(built.g, opts);
1878 ASSERT_FALSE(r.is_planar);
1879 ASSERT_TRUE(r.has_nonplanar_certificate);
1880
1881 const std::string graphml_path = make_tmp_path("external_render_svg", ".graphml");
1882 const std::string output_dir = make_tmp_path("external_render_svg_dir", "");
1884
1885 const std::string lbl = "render_svg";
1887 {graphml_path}, false, false, false, false, "",
1888 "--render-gephi --require-render "
1889 "--render-profile portable.ruby-render-svg "
1890 "--render-output-dir " + shell_quote(output_dir),
1891 lbl);
1892 EXPECT_EQ(rc, 0);
1893
1894 const fs::path out_path = fs::path(output_dir) /
1895 (fs::path(graphml_path).stem().string() + ".svg");
1896 EXPECT_TRUE(fs::exists(out_path));
1897 EXPECT_GT(fs::file_size(out_path), 0u);
1898
1899 const std::string out = read_text_file(validator_stdout_path(lbl));
1900 EXPECT_NE(out.find("render_output_kind"), std::string::npos);
1901 EXPECT_NE(out.find("svg"), std::string::npos);
1902}
1903
1904
1906{
1907 namespace fs = std::filesystem;
1908
1910 {
1911 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1912 return;
1913 }
1914
1916
1919 const auto r = planarity_test(built.g, opts);
1920 ASSERT_FALSE(r.is_planar);
1921 ASSERT_TRUE(r.has_nonplanar_certificate);
1922
1923 const std::string graphml_path = make_tmp_path("external_render_pdf", ".graphml");
1924 const std::string output_dir = make_tmp_path("external_render_pdf_dir", "");
1926
1928 {graphml_path}, false, false, false, false, "",
1929 "--render-gephi --require-render "
1930 "--render-profile portable.ruby-render-pdf "
1931 "--render-output-dir " + shell_quote(output_dir));
1932 EXPECT_EQ(rc, 0);
1933
1934 const fs::path out_path = fs::path(output_dir) /
1935 (fs::path(graphml_path).stem().string() + ".pdf");
1936 EXPECT_TRUE(fs::exists(out_path));
1937 EXPECT_GT(fs::file_size(out_path), 0u);
1938}
1939
1940
1942{
1943 namespace fs = std::filesystem;
1944
1946 {
1947 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1948 return;
1949 }
1950
1952
1955 const auto r = planarity_test(built.g, opts);
1956 ASSERT_FALSE(r.is_planar);
1957 ASSERT_TRUE(r.has_nonplanar_certificate);
1958
1959 const fs::path dir = fs::temp_directory_path() / "aleph planarity render spaces";
1960 fs::create_directories(dir);
1961 const std::string graphml_path = (dir / "k33 certificate.graphml").string();
1962 const std::string output_dir = (dir / "render output").string();
1964
1966 {graphml_path}, false, false, false, false, "",
1967 "--render-gephi --require-render "
1968 "--render-profile portable.ruby-render-svg "
1969 "--render-output-dir " + shell_quote(output_dir));
1970 EXPECT_EQ(rc, 0);
1971
1972 const fs::path out_path = fs::path(output_dir) /
1973 (fs::path(graphml_path).stem().string() + ".svg");
1974 EXPECT_TRUE(fs::exists(out_path));
1975 EXPECT_GT(fs::file_size(out_path), 0u);
1976}
1977
1978
1980{
1982 {
1983 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
1984 return;
1985 }
1986
1988
1991 const auto r = planarity_test(built.g, opts);
1992 ASSERT_FALSE(r.is_planar);
1993 ASSERT_TRUE(r.has_nonplanar_certificate);
1994
1995 const std::string graphml_path = make_tmp_path("external_render_bad", ".graphml");
1997
1999 {graphml_path}, false, false, false, false, "",
2000 "--render-gephi --require-render --render-profile missing.profile");
2001 EXPECT_NE(rc, 0);
2002}
2003
2004
2006{
2008
2011 const auto r = planarity_test(built.g, opts);
2012 ASSERT_FALSE(r.is_planar);
2013 ASSERT_TRUE(r.has_nonplanar_certificate);
2014
2015 const std::string graphml_path = make_tmp_path("external_batch", ".graphml");
2016 const std::string gexf_path = make_tmp_path("external_batch", ".gexf");
2017 const std::string report_path = make_tmp_path("external_batch_report", ".json");
2018
2021
2024 "--gephi --require-gephi --gephi-template portable.ruby-file-exists --print-summary");
2025 EXPECT_EQ(rc, 0);
2026
2027 const std::string report = read_text_file(report_path);
2028 EXPECT_NE(report.find("\"num_inputs\": 2"), std::string::npos);
2029 EXPECT_NE(report.find("\"overall_valid\": true"), std::string::npos);
2030 EXPECT_NE(report.find("\"gephi_template\": \"portable.ruby-file-exists\""),
2031 std::string::npos);
2032}
2033
2034
2036{
2038
2041 const auto r = planarity_test(built.g, opts);
2042 ASSERT_FALSE(r.is_planar);
2043 ASSERT_TRUE(r.has_nonplanar_certificate);
2044
2045 const std::string graphml_path = make_tmp_path("external_batch_render", ".graphml");
2046 const std::string report_path = make_tmp_path("external_batch_render_report", ".json");
2047 const std::string output_dir = make_tmp_path("external_batch_render_dir", "");
2049
2052 "--render-gephi --require-render "
2053 "--render-profile portable.ruby-render-svg "
2054 "--render-output-dir " + shell_quote(output_dir));
2055 EXPECT_EQ(rc, 0);
2056
2057 const std::string report = read_text_file(report_path);
2058 EXPECT_NE(report.find("\"render_profile\": \"portable.ruby-render-svg\""),
2059 std::string::npos);
2060 EXPECT_NE(report.find("\"overall_valid\": true"), std::string::npos);
2061}
2062
2063
2065{
2067
2070 const auto r = planarity_test(built.g, opts);
2071 ASSERT_FALSE(r.is_planar);
2072 ASSERT_TRUE(r.has_nonplanar_certificate);
2073
2074 const std::string graphml_path = make_tmp_path("external_visual", ".graphml");
2075 const std::string report_path = make_tmp_path("external_visual_report", ".json");
2076 const std::string output_dir = make_tmp_path("external_visual_render_dir", "");
2078
2080 {graphml_path},
2081 {"portable.ruby-render-svg", "portable.ruby-render-pdf"},
2083 "--render-output-dir " + shell_quote(output_dir) + " --print-summary");
2084 EXPECT_EQ(rc, 0);
2085
2086 const std::string report = read_text_file(report_path);
2087 EXPECT_NE(report.find("\"overall_passed\": true"), std::string::npos);
2088 EXPECT_NE(report.find("\"profile_id\": \"portable.ruby-render-svg\""),
2089 std::string::npos);
2090 EXPECT_NE(report.find("\"profile_id\": \"portable.ruby-render-pdf\""),
2091 std::string::npos);
2092}
2093
2094
2096{
2098
2101 const auto r = planarity_test(built.g, opts);
2102 ASSERT_FALSE(r.is_planar);
2103 ASSERT_TRUE(r.has_nonplanar_certificate);
2104
2105 const std::string graphml_path = make_tmp_path("external_visual_bad", ".graphml");
2106 const std::string report_path = make_tmp_path("external_visual_bad_report", ".json");
2107 const std::string output_dir = make_tmp_path("external_visual_bad_render_dir", "");
2108 const std::string manifest_path = make_tmp_path("external_visual_bad_manifest", ".json");
2112 R"({
2113 "schema_version": 1,
2114 "entries": [
2115 {
2116 "profile_id": "portable.ruby-render-svg",
2117 "output_ext": "svg",
2118 "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
2119 "bytes": 1
2120 }
2121 ]
2122}
2123)");
2124
2126 {graphml_path},
2127 {"portable.ruby-render-svg"},
2129 "--render-output-dir " + shell_quote(output_dir)
2130 + " --golden-manifest " + shell_quote(manifest_path));
2131 EXPECT_NE(rc, 0);
2132
2133 const std::string report = read_text_file(report_path);
2134 EXPECT_NE(report.find("\"overall_passed\": false"), std::string::npos);
2135 EXPECT_NE(report.find("Golden hash mismatch"), std::string::npos);
2136}
2137
2138
2140{
2142
2145 const auto r = planarity_test(built.g, opts);
2146 ASSERT_FALSE(r.is_planar);
2147 ASSERT_TRUE(r.has_nonplanar_certificate);
2148
2149 const std::string graphml_path = make_tmp_path("external_visual_upd", ".graphml");
2150 const std::string report_path = make_tmp_path("external_visual_upd_report", ".json");
2151 const std::string output_dir = make_tmp_path("external_visual_upd_render_dir", "");
2152 const std::string manifest_path = make_tmp_path("external_visual_upd_manifest", ".json");
2154
2156 {graphml_path},
2157 {"portable.ruby-render-svg"},
2159 "--render-output-dir " + shell_quote(output_dir)
2160 + " --golden-manifest " + shell_quote(manifest_path)
2161 + " --update-golden");
2162 EXPECT_EQ(rc, 0);
2163
2164 const std::string report = read_text_file(report_path);
2165 const std::string manifest = read_text_file(manifest_path);
2166
2167 if (manifest.find("portable.ruby-render-svg") == std::string::npos)
2168 {
2169 std::cerr << "[diag] manifest_path=" << manifest_path << "\n"
2170 << "[diag] manifest content (" << manifest.size() << " bytes):\n"
2171 << manifest << "\n"
2172 << "[diag] report content (" << report.size() << " bytes):\n"
2173 << report << "\n";
2174 }
2175
2176 EXPECT_NE(manifest.find("\"profile_id\": \"portable.ruby-render-svg\""),
2177 std::string::npos);
2178 EXPECT_NE(manifest.find("0459f595d6268e7bdf9e8e273d4394fa50d23a89ec3d2449d10b7b47941b1327"),
2179 std::string::npos);
2180}
2181
2182
2184{
2185 namespace fs = std::filesystem;
2186
2187 const fs::path artifacts_root =
2188 fs::path(make_tmp_path("gephi_nightly_artifacts_ok", ""));
2189 fs::create_directories(artifacts_root);
2190
2191 const auto write_case = [&](const std::string & dir_name,
2192 const std::string & os_name,
2193 const std::string & tag,
2194 const int validator_exit_code,
2195 const bool overall_valid,
2196 const std::string & probe_kind = "launcher")
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_probe_kind=" + probe_kind + "\n"
2206 "gephi_executable=/tmp/gephi\n");
2207
2208 std::ostringstream summary;
2209 summary << "{\n"
2210 << " \"os\": \"" << os_name << "\",\n"
2211 << " \"gephi_tag\": \"" << tag << "\",\n"
2212 << " \"asset_name\": \"gephi-package-" << tag << ".zip\",\n"
2213 << " \"gephi_probe_kind\": \"" << probe_kind << "\",\n"
2214 << " \"validator_exit_code\": " << validator_exit_code << ",\n"
2215 << " \"overall_valid\": " << (overall_valid ? "true" : "false") << "\n"
2216 << "}\n";
2217 write_text_file((dir / "weekly_case_summary.json").string(), summary.str());
2218 };
2219
2220 write_case("gephi-nightly-ubuntu-24.04-v0.9.7", "ubuntu-24.04", "v0.9.7", 0, true);
2221 write_case("gephi-nightly-ubuntu-24.04-v0.10.1", "ubuntu-24.04", "v0.10.1", 0, true);
2222 write_case("gephi-nightly-windows-2022-v0.9.7", "windows-2022", "v0.9.7", 0, true,
2223 "installer");
2224 write_case("gephi-nightly-windows-2022-v0.10.1", "windows-2022", "v0.10.1", 0, true,
2225 "installer");
2226
2227 const std::string report_json = make_tmp_path("gephi_nightly_report_ok", ".json");
2228 const std::string report_md = make_tmp_path("gephi_nightly_report_ok", ".md");
2229
2230 const int rc = run_gephi_nightly_comparison(
2231 artifacts_root.string(), report_json, report_md, "--print-summary");
2232 EXPECT_EQ(rc, 0);
2233
2234 const std::string json = read_text_file(report_json);
2235 EXPECT_NE(json.find("\"num_entries\": 4"), std::string::npos);
2236 EXPECT_NE(json.find("\"num_regressions\": 0"), std::string::npos);
2237 EXPECT_NE(json.find("\"has_regressions\": false"), std::string::npos);
2238 EXPECT_NE(json.find("\"gephi_probe_kind\": \"launcher\""), std::string::npos);
2239 EXPECT_NE(json.find("\"gephi_probe_kind\": \"installer\""), std::string::npos);
2240
2241 const std::string markdown = read_text_file(report_md);
2242 EXPECT_NE(markdown.find("### Regression Alerts"), std::string::npos);
2243 EXPECT_NE(markdown.find("- none"), std::string::npos);
2244 EXPECT_NE(markdown.find("| Gephi tag | OS | valid | exit_code | probe |"),
2245 std::string::npos);
2246}
2247
2248
2250{
2251 namespace fs = std::filesystem;
2252
2253 const fs::path artifacts_root =
2254 fs::path(make_tmp_path("gephi_nightly_artifacts_bad", ""));
2255 fs::create_directories(artifacts_root);
2256
2257 const auto write_case = [&](const std::string & dir_name,
2258 const std::string & os_name,
2259 const std::string & tag,
2260 const int validator_exit_code,
2261 const bool overall_valid,
2262 const std::string & probe_kind = "launcher")
2263 {
2264 const fs::path dir = artifacts_root / dir_name;
2265 fs::create_directories(dir);
2266
2267 write_text_file((dir / "downloaded_gephi_asset.txt").string(),
2268 "tag=" + tag + "\n"
2269 "runner_os=" + os_name + "\n"
2270 "asset_name=gephi-package-" + tag + ".zip\n"
2271 "gephi_probe_kind=" + probe_kind + "\n"
2272 "gephi_executable=/tmp/gephi\n");
2273
2274 std::ostringstream summary;
2275 summary << "{\n"
2276 << " \"os\": \"" << os_name << "\",\n"
2277 << " \"gephi_tag\": \"" << tag << "\",\n"
2278 << " \"asset_name\": \"gephi-package-" << tag << ".zip\",\n"
2279 << " \"gephi_probe_kind\": \"" << probe_kind << "\",\n"
2280 << " \"validator_exit_code\": " << validator_exit_code << ",\n"
2281 << " \"overall_valid\": " << (overall_valid ? "true" : "false") << "\n"
2282 << "}\n";
2283 write_text_file((dir / "weekly_case_summary.json").string(), summary.str());
2284 };
2285
2286 write_case("gephi-nightly-ubuntu-24.04-v0.9.7", "ubuntu-24.04", "v0.9.7", 0, true);
2287 write_case("gephi-nightly-ubuntu-24.04-v0.10.1", "ubuntu-24.04", "v0.10.1", 2, false);
2288
2289 const std::string report_json = make_tmp_path("gephi_nightly_report_bad", ".json");
2290 const std::string report_md = make_tmp_path("gephi_nightly_report_bad", ".md");
2291
2292 const int rc = run_gephi_nightly_comparison(
2293 artifacts_root.string(), report_json, report_md, "--print-summary");
2294 EXPECT_EQ(rc, 0);
2295
2296 const std::string json = read_text_file(report_json);
2297 EXPECT_NE(json.find("\"num_regressions\": 1"), std::string::npos);
2298 EXPECT_NE(json.find("\"has_regressions\": true"), std::string::npos);
2299 EXPECT_NE(json.find("overall_valid_regressed"), std::string::npos);
2300 EXPECT_NE(json.find("exit_code_regressed"), std::string::npos);
2301
2303 artifacts_root.string(), report_json, report_md, "--fail-on-regressions");
2304 EXPECT_NE(rc_fail, 0);
2305}
2306
2307
2309{
2310 const std::string report_json = make_tmp_path("gephi_notify_ok_report", ".json");
2311 const std::string output_md = make_tmp_path("gephi_notify_ok_alert", ".md");
2312
2315 R"({
2316 "schema_version": 1,
2317 "run_id": "r1",
2318 "run_attempt": "1",
2319 "git_sha": "abc123",
2320 "resolved_tags": "v0.9.7,v0.10.1",
2321 "num_entries": 4,
2322 "entries": [],
2323 "num_regressions": 0,
2324 "has_regressions": false,
2325 "regressions": []
2326}
2327)");
2328
2329 const int rc = run_gephi_regression_notify(report_json, output_md, "--print-summary");
2330 EXPECT_EQ(rc, 0);
2331
2332 const std::string markdown = read_text_file(output_md);
2333 EXPECT_NE(markdown.find("Gephi Nightly Regression Alert"), std::string::npos);
2334 EXPECT_NE(markdown.find("No regressions detected."), std::string::npos);
2335}
2336
2337
2339{
2340 const std::string report_json = make_tmp_path("gephi_notify_bad_report", ".json");
2341 const std::string output_md = make_tmp_path("gephi_notify_bad_alert", ".md");
2342
2345 R"({
2346 "schema_version": 1,
2347 "run_id": "r2",
2348 "run_attempt": "1",
2349 "git_sha": "def456",
2350 "resolved_tags": "v0.9.7,v0.10.1",
2351 "num_entries": 2,
2352 "entries": [],
2353 "num_regressions": 1,
2354 "has_regressions": true,
2355 "regressions": [
2356 {
2357 "os": "ubuntu-24.04",
2358 "from_tag": "v0.9.7",
2359 "to_tag": "v0.10.1",
2360 "from_overall_valid": true,
2361 "to_overall_valid": false,
2362 "from_exit_code": 0,
2363 "to_exit_code": 2,
2364 "reason_codes": ["overall_valid_regressed", "exit_code_regressed"]
2365 }
2367}
2368)");
2369
2371 report_json, output_md, "--print-summary");
2373
2374 const std::string markdown = read_text_file(output_md);
2375 EXPECT_NE(markdown.find("Detected regression(s):"), std::string::npos);
2376 EXPECT_NE(markdown.find("overall_valid_regressed"), std::string::npos);
2377 EXPECT_NE(markdown.find("exit_code_regressed"), std::string::npos);
2378 EXPECT_NE(markdown.find("https://example.invalid/run/123"), std::string::npos);
2379
2381 report_json, output_md, "--require-webhook-success");
2383}
2384
2385
2387{
2388 const std::string broken_graphml = make_tmp_path("external_batch_invalid", ".graphml");
2389 const std::string report_path = make_tmp_path("external_batch_invalid_report", ".json");
2390
2391 write_text_file(broken_graphml, R"(<?xml version="1.0" encoding="UTF-8"?>
2392<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
2393 <graph id="Broken" edgedefault="undirected">
2394 <node id="n0"/>
2395 <node id="n1"/>
2396 <edge id="e0" source="n0" target="n1">
2397 <data key="k_edge_kind">path</data>
2398 </edge>
2399 </graph>
2400</graphml>
2401)");
2402
2404 EXPECT_NE(rc, 0);
2405
2406 const std::string report = read_text_file(report_path);
2407 EXPECT_NE(report.find("\"num_inputs\": 1"), std::string::npos);
2408 EXPECT_NE(report.find("\"overall_valid\": false"), std::string::npos);
2409}
2410
2411
2413{
2414 namespace fs = std::filesystem;
2415
2417 {
2418 GTEST_SKIP() << "Skipped: Ruby or planarity_certificate_validator.rb not found";
2419 return;
2420 }
2421
2422 const auto root = find_repo_root();
2423 ASSERT_TRUE(root.has_value());
2424
2425 const fs::path fixture =
2426 *root / "scripts" / "fixtures" / "planarity_k33_certificate.graphml";
2427 ASSERT_TRUE(fs::exists(fixture));
2428
2429 const int rc = run_external_certificate_validator({fixture.string()});
2430 EXPECT_EQ(rc, 0);
2431}
2432
2433
2435{
2437
2438 const auto r = planarity_test(built.g); // no certificate requested
2439 ASSERT_FALSE(r.is_planar);
2440 ASSERT_FALSE(r.has_nonplanar_certificate);
2441
2446
2447 const auto vr = validate_nonplanar_certificate(r);
2448 EXPECT_FALSE(vr.has_certificate);
2449 EXPECT_FALSE(vr.is_valid);
2451}
2452// satisfy CI policy
Planarity testing on Aleph graphs.
long double vr
Definition btreepic.C:150
long double w
Definition btreepic.C:153
size_t size_t int32_t * out
Definition ca-c-api.h:120
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:138
Generic directed graph (digraph) wrapper template.
Definition graph-dry.H:3960
Graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:429
_Graph_Node Node
The graph type.
Definition tpl_graph.H:433
_Graph_Arc Arc
The node class type.
Definition tpl_graph.H:434
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:4071
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 a comprehensive 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())
High-level boolean check for graph 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.
size_t blossom_maximum_cardinality_matching(const GT &g, DynDlist< typename GT::Arc * > &matching, SA sa=SA())
Alias of compute_maximum_cardinality_general_matching().
Definition Blossom.H:466
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().
std::filesystem::path make_tmp_path(const std::filesystem::path &target)
Generate a temporary sibling path for atomic writes.
std::string shell_quote(const std::string &s)
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
size_t size(Node *root) noexcept
and
Check uniqueness with explicit hash + equality functors.
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:223
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 options for planarity testing and auxiliary outputs.
bool embedding_allow_bruteforce_fallback
If the LR heuristic fails to find a valid embedding, allows falling back to an exact but potentially ...
bool compute_embedding
If true and the graph is found to be planar, computes a combinatorial embedding (rotation system and ...
bool compute_nonplanar_certificate
If true and the graph is found to be non-planar, attempts to extract a Kuratowski subdivision (K5 or ...
Description of an edge participating in a witness.
Description of a path participating in a Kuratowski witness.
FooMap m(5, fst_unit_pair_hash, snd_unit_pair_hash)
CmdLine cmd
Definition testHash.C:48
gsl_rng * r
Array-based graph implementation.
Generic graph and digraph implementations.