|
| 1 | +/* |
| 2 | + * Headless tests for layout logic (no GTK). We simulate a subset of the |
| 3 | + * geometry algorithms used by DisplaysOverlay and VirtualMonitor to ensure |
| 4 | + * correctness with 3+ monitors and edge/overlap cases. |
| 5 | + */ |
| 6 | + |
| 7 | +public class TestVM : GLib.Object { |
| 8 | + public int x { get; set; } |
| 9 | + public int y { get; set; } |
| 10 | + public int w { get; set; } |
| 11 | + public int h { get; set; } |
| 12 | + |
| 13 | + public TestVM (int x, int y, int w, int h) { |
| 14 | + this.x = x; this.y = y; this.w = w; this.h = h; |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +namespace Layout { |
| 19 | + public void set_origin_zero (GLib.List<TestVM> vms) { |
| 20 | + int min_x = int.MAX; |
| 21 | + int min_y = int.MAX; |
| 22 | + foreach (unowned var vm in vms) { |
| 23 | + min_x = int.min (min_x, vm.x); |
| 24 | + min_y = int.min (min_y, vm.y); |
| 25 | + } |
| 26 | + if (min_x == 0 && min_y == 0) return; |
| 27 | + foreach (unowned var vm in vms) { |
| 28 | + vm.x -= min_x; vm.y -= min_y; |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + public bool intersects (TestVM a, TestVM b, out int ovw, out int ovh) { |
| 33 | + int ax2 = a.x + a.w, ay2 = a.y + a.h; |
| 34 | + int bx2 = b.x + b.w, by2 = b.y + b.h; |
| 35 | + ovw = int.max (0, int.min (ax2, bx2) - int.max (a.x, b.x)); |
| 36 | + ovh = int.max (0, int.min (ay2, by2) - int.max (a.y, b.y)); |
| 37 | + return ovw > 0 && ovh > 0; |
| 38 | + } |
| 39 | + |
| 40 | + // Resolve overlaps by moving B minimally away from A along smaller overlap axis |
| 41 | + public bool resolve_overlap_once (TestVM a, TestVM b) { |
| 42 | + int ovw, ovh; |
| 43 | + if (!intersects (a, b, out ovw, out ovh)) return false; |
| 44 | + if (ovw <= ovh) { |
| 45 | + if (b.x < a.x) b.x -= ovw; else b.x += ovw; |
| 46 | + } else { |
| 47 | + if (b.y < a.y) b.y -= ovh; else b.y += ovh; |
| 48 | + } |
| 49 | + return true; |
| 50 | + } |
| 51 | + |
| 52 | + public void resolve_all_overlaps (GLib.List<TestVM> vms, uint max_iter = 16) { |
| 53 | + uint iter = 0; |
| 54 | + while (iter++ < max_iter) { |
| 55 | + bool moved = false; |
| 56 | + for (int i = 0; i < (int) vms.length (); i++) { |
| 57 | + for (int j = i + 1; j < (int) vms.length (); j++) { |
| 58 | + moved = resolve_overlap_once (vms.nth_data (i), vms.nth_data (j)) || moved; |
| 59 | + } |
| 60 | + } |
| 61 | + if (!moved) break; |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + public bool is_connected_pair (TestVM a, TestVM b) { |
| 66 | + // Adjoin: touch on an edge (inclusive) without overlapping interior |
| 67 | + bool x_adjacent = (a.x + a.w == b.x) || (b.x + b.w == a.x); |
| 68 | + bool y_overlap = !(a.y + a.h <= b.y || b.y + b.h <= a.y); |
| 69 | + bool y_adjacent = (a.y + a.h == b.y) || (b.y + b.h == a.y); |
| 70 | + bool x_overlap = !(a.x + a.w <= b.x || b.x + b.w <= a.x); |
| 71 | + return (x_adjacent && y_overlap) || (y_adjacent && x_overlap); |
| 72 | + } |
| 73 | + |
| 74 | + public bool is_connected_all (GLib.List<TestVM> vms) { |
| 75 | + if (vms.length () <= 1) return true; |
| 76 | + var seen = new GLib.HashTable<TestVM,bool> (GLib.direct_hash, GLib.direct_equal); |
| 77 | + var queue = new GLib.Queue<TestVM> (); |
| 78 | + var first = vms.nth_data (0); |
| 79 | + seen.insert (first, true); |
| 80 | + queue.push_tail (first); |
| 81 | + while (!queue.is_empty ()) { |
| 82 | + var cur = queue.pop_head (); |
| 83 | + foreach (unowned var vm in vms) { |
| 84 | + if (seen.lookup (vm)) continue; |
| 85 | + if (is_connected_pair (cur, vm)) { |
| 86 | + seen.insert (vm, true); |
| 87 | + queue.push_tail (vm); |
| 88 | + } |
| 89 | + } |
| 90 | + } |
| 91 | + return seen.size () == vms.length (); |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +int failures = 0; |
| 96 | +void assert_true (bool cond, string msg) { |
| 97 | + if (!cond) { |
| 98 | + critical ("Assertion failed: %s", msg); |
| 99 | + failures++; |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +int main (string[] args) { |
| 104 | + // Test 1: Three displays, initially overlapping; resolve, normalize, and validate connectivity |
| 105 | + var vms = new GLib.List<TestVM> (); |
| 106 | + vms.append (new TestVM (0, 0, 1920, 1080)); |
| 107 | + vms.append (new TestVM (1800, 0, 1920, 1080)); // overlaps 120px on X |
| 108 | + vms.append (new TestVM (3600, 100, 1280, 1024)); // slightly below and to the right |
| 109 | + |
| 110 | + Layout.resolve_all_overlaps (vms); |
| 111 | + Layout.set_origin_zero (vms); |
| 112 | + int _ovw, _ovh; |
| 113 | + assert_true (!Layout.intersects (vms.nth_data (0), vms.nth_data (1), out _ovw, out _ovh), "VM0/VM1 should not overlap after resolve"); |
| 114 | + assert_true (!Layout.intersects (vms.nth_data (1), vms.nth_data (2), out _ovw, out _ovh), "VM1/VM2 should not overlap after resolve"); |
| 115 | + |
| 116 | + // Expect connectivity after minor adjustments |
| 117 | + assert_true (Layout.is_connected_all (vms), "All VMs should be connected"); |
| 118 | + |
| 119 | + // Test 2: Vertical stacking with same X, ensure normalization keeps origin at (0,0) |
| 120 | + var v2 = new GLib.List<TestVM> (); |
| 121 | + v2.append (new TestVM (100, 200, 1600, 900)); |
| 122 | + v2.append (new TestVM (100, 1100, 1600, 900)); |
| 123 | + Layout.set_origin_zero (v2); |
| 124 | + assert_true (v2.nth_data (0).x == 0 && v2.nth_data (0).y == 0, "Origin normalized to (0,0)"); |
| 125 | + assert_true (v2.nth_data (1).x == 0 && v2.nth_data (1).y == 900, "Second stacked below first at y=height"); |
| 126 | + |
| 127 | + // Test 3: Edge adjacency detection |
| 128 | + var a = new TestVM (0, 0, 100, 100); |
| 129 | + var b = new TestVM (100, 10, 100, 50); // touches a's right edge |
| 130 | + assert_true (Layout.is_connected_pair (a, b), "Edge adjacency should be connected"); |
| 131 | + |
| 132 | + // Test 4: Same Y alignment with exact adjacency across three displays |
| 133 | + var t4 = new GLib.List<TestVM> (); |
| 134 | + t4.append (new TestVM (0, 0, 1000, 800)); |
| 135 | + t4.append (new TestVM (1000, 0, 1000, 800)); |
| 136 | + t4.append (new TestVM (2000, 0, 1000, 800)); |
| 137 | + assert_true (!Layout.intersects (t4.nth_data (0), t4.nth_data (1), out _ovw, out _ovh), "T4: 0/1 no overlap"); |
| 138 | + assert_true (!Layout.intersects (t4.nth_data (1), t4.nth_data (2), out _ovw, out _ovh), "T4: 1/2 no overlap"); |
| 139 | + assert_true (Layout.is_connected_all (t4), "T4: all connected along same Y"); |
| 140 | + |
| 141 | + // Test 5: Same Y with overlaps; resolver should separate into no-overlap configuration and keep connectivity |
| 142 | + var t5 = new GLib.List<TestVM> (); |
| 143 | + t5.append (new TestVM (0, 0, 1000, 800)); |
| 144 | + t5.append (new TestVM (900, 0, 1000, 800)); // overlaps with first by 100px |
| 145 | + t5.append (new TestVM (1900, 0, 1000, 800)); // overlaps with second by 0px (adjacent or slight overlap if math changes) |
| 146 | + Layout.resolve_all_overlaps (t5); |
| 147 | + assert_true (!Layout.intersects (t5.nth_data (0), t5.nth_data (1), out _ovw, out _ovh), "T5: 0/1 resolved"); |
| 148 | + assert_true (!Layout.intersects (t5.nth_data (1), t5.nth_data (2), out _ovw, out _ovh), "T5: 1/2 resolved"); |
| 149 | + assert_true (Layout.is_connected_all (t5), "T5: all connected after resolve"); |
| 150 | + |
| 151 | + // Test 6: Vertical stacking with same X (three displays) |
| 152 | + var t6 = new GLib.List<TestVM> (); |
| 153 | + t6.append (new TestVM (0, 0, 1200, 900)); |
| 154 | + t6.append (new TestVM (0, 900, 1200, 900)); |
| 155 | + t6.append (new TestVM (0, 1800, 1200, 900)); |
| 156 | + assert_true (!Layout.intersects (t6.nth_data (0), t6.nth_data (1), out _ovw, out _ovh), "T6: 0/1 stacked no overlap"); |
| 157 | + assert_true (!Layout.intersects (t6.nth_data (1), t6.nth_data (2), out _ovw, out _ovh), "T6: 1/2 stacked no overlap"); |
| 158 | + assert_true (Layout.is_connected_all (t6), "T6: stacked connected"); |
| 159 | + |
| 160 | + // If we reached here, all tests passed |
| 161 | + if (failures == 0) message ("layout tests passed"); |
| 162 | + return failures == 0 ? 0 : 1; |
| 163 | +} |
0 commit comments