1
|
|
|
#!/usr/bin/env python |
2
|
|
|
# -*- coding: utf-8 -*- |
3
|
|
|
"""Unit Test Suite for QuickTile using Nose test discovery""" |
4
|
|
|
|
5
|
|
|
__author__ = "Stephan Sokolow (deitarion/SSokolow)" |
6
|
|
|
__license__ = "GNU GPL 2.0 or later" |
7
|
|
|
|
8
|
|
|
try: |
9
|
|
|
import pygtk |
10
|
|
|
pygtk.require('2.0') |
11
|
|
|
except ImportError as err: |
12
|
|
|
# Apparently Travis-CI's build environment doesn't add this |
13
|
|
|
import sys |
14
|
|
|
sys.path.append('/usr/lib/python2.7/dist-packages') |
15
|
|
|
sys.path.append('/usr/lib/python2.7/dist-packages/gtk-2.0') |
16
|
|
|
|
17
|
|
|
import logging, operator, sys |
18
|
|
|
|
19
|
|
|
import gtk |
20
|
|
|
import gtk.gdk, wnck # pylint: disable=import-error |
21
|
|
|
|
22
|
|
|
from quicktile import commands, wm |
23
|
|
|
from quicktile.util import powerset, EnumSafeDict, XInitError |
24
|
|
|
|
25
|
|
|
# Ensure code coverage is accurate |
26
|
|
|
from quicktile import __main__ # pylint: disable=unused-import |
27
|
|
|
|
28
|
|
|
# Silence flake8 since PyLint already took the line comment spot |
29
|
|
|
__main__ # pylint: disable=pointless-statement |
30
|
|
|
|
31
|
|
|
log = logging.getLogger(__name__) |
32
|
|
|
|
33
|
|
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7: # pragma: no cover |
34
|
|
|
import unittest2 as unittest |
35
|
|
|
else: # pragma: no cover |
36
|
|
|
import unittest |
37
|
|
|
|
38
|
|
|
# pylint: disable=too-few-public-methods |
39
|
|
|
|
40
|
|
|
class ComplainingEnum(object): |
41
|
|
|
"""A parent class for classes which should raise C{TypeError} when compared |
42
|
|
|
|
43
|
|
|
(A stricter version of the annoyance I observed in Glib enums.) |
44
|
|
|
""" |
45
|
|
|
def __init__(self, testcase): |
46
|
|
|
self.testcase = testcase |
47
|
|
|
|
48
|
|
|
def __cmp__(self, other): |
49
|
|
|
"""Raises an exception if comparing against another type. |
50
|
|
|
@raises TypeError: C{type(self) != type(other)} |
51
|
|
|
@returns: C{id(self) == id(other)} |
52
|
|
|
@rtype: C{bool} |
53
|
|
|
""" |
54
|
|
|
if type(self) != type(other): # pylint: disable=unidiomatic-typecheck |
55
|
|
|
raise TypeError("Should not be comparing heterogeneous enums: " |
56
|
|
|
"%s != %s" % (type(self), type(other))) |
57
|
|
|
else: |
58
|
|
|
return cmp(id(self), id(other)) |
59
|
|
|
|
60
|
|
|
class Thing1(ComplainingEnum): |
61
|
|
|
"""See L{ComplainingEnum}""" |
62
|
|
|
class Thing2(ComplainingEnum): |
63
|
|
|
"""See L{ComplainingEnum}""" |
64
|
|
|
|
65
|
|
|
class TestCommandRegistry(unittest.TestCase): |
66
|
|
|
"""Tests for the CommandRegistry class""" |
67
|
|
|
def setUp(self): |
68
|
|
|
self.registry = commands.CommandRegistry() |
69
|
|
|
|
70
|
|
|
# TODO: Implement tests for CommandRegistry |
71
|
|
|
|
72
|
|
|
# TODO: Implement tests for cycle_dimensions |
73
|
|
|
# TODO: Implement tests for cycle_monitors |
74
|
|
|
# TODO: Implement tests for move_to_position |
75
|
|
|
# TODO: Implement tests for toggle_decorated |
76
|
|
|
# TODO: Implement tests for toggle_desktop |
77
|
|
|
# TODO: Implement tests for toggle_state |
78
|
|
|
# TODO: Implement tests for trigger_keyboard_action |
79
|
|
|
# TODO: Implement tests for workspace_go |
80
|
|
|
# TODO: Implement tests for workspace_send_window |
81
|
|
|
|
82
|
|
|
class TestEnumSafeDict(unittest.TestCase): |
83
|
|
|
"""Tests to ensure EnumSafeDict never compares enums of different types""" |
84
|
|
|
def setUp(self): |
85
|
|
|
self.thing1 = Thing1(self) |
86
|
|
|
self.thing2 = Thing2(self) |
87
|
|
|
|
88
|
|
|
self.test_mappings = [ |
89
|
|
|
(self.thing1, 'a'), |
90
|
|
|
(self.thing2, 'b'), |
91
|
|
|
(1, self.thing1), |
92
|
|
|
(2, self.thing2) |
93
|
|
|
] |
94
|
|
|
|
95
|
|
|
self.empty = EnumSafeDict() |
96
|
|
|
self.full = EnumSafeDict( |
97
|
|
|
*[dict([x]) for x in self.test_mappings]) |
98
|
|
|
|
99
|
|
|
def test_testing_shims(self): |
100
|
|
|
"""EnumSafeDict: Testing shims function correctly""" |
101
|
|
|
for oper in ('lt', 'le', 'eq', 'ne', 'ge', 'gt'): |
102
|
|
|
with self.assertRaises(TypeError): |
103
|
|
|
print "Testing %s..." % oper |
104
|
|
|
getattr(operator, oper)(self.thing1, self.thing2) |
105
|
|
|
|
106
|
|
|
def test_init_with_content(self): |
107
|
|
|
"""EnumSafeDict: Initialization with content""" |
108
|
|
|
|
109
|
|
|
test_map = self.test_mappings[:] |
110
|
|
|
|
111
|
|
|
while test_map: |
112
|
|
|
key, val = test_map.pop() |
113
|
|
|
self.assertEqual(self.full[key], val, |
114
|
|
|
"All things in the input must make it into EnumSafeDict: " + |
115
|
|
|
str(key)) |
116
|
|
|
|
117
|
|
|
self.assertFalse(test_map, "EnumSafeDict must contain ONLY things from" |
118
|
|
|
" the input.") |
119
|
|
|
|
120
|
|
|
def test_get_set_del(self): |
121
|
|
|
"""EnumSafeDict: get/set/delitem""" |
122
|
|
|
|
123
|
|
|
# Test the "no matching key" branch of __getitem__ |
124
|
|
|
with self.assertRaises(KeyError): |
125
|
|
|
self.empty['nonexist'] # pylint: disable=pointless-statement |
126
|
|
|
|
127
|
|
|
# Let Thing1 and Thing2 error out if they're compared in __setitem__ |
128
|
|
|
for key, val in self.test_mappings: |
129
|
|
|
self.empty[key] = val |
130
|
|
|
|
131
|
|
|
# Test the "matching key" branch of __getitem__ and __delitem__ |
132
|
|
|
for key, val in self.test_mappings: |
133
|
|
|
assert self.empty[key] == val |
134
|
|
|
del self.empty[key] |
135
|
|
|
with self.assertRaises(KeyError): |
136
|
|
|
self.empty[key] # pylint: disable=pointless-statement |
137
|
|
|
|
138
|
|
|
# TODO: Complete set of tests which try to trick EnumSafeDict into |
139
|
|
|
# comparing thing1 and thing2. |
140
|
|
|
|
141
|
|
|
|
142
|
|
|
# TODO: Implement tests for GravityLayout |
143
|
|
|
|
144
|
|
|
# TODO: Implement tests for KeyBinder |
145
|
|
|
|
146
|
|
|
# TODO: Implement tests for QuickTileApp |
147
|
|
|
|
148
|
|
|
class TestHelpers(unittest.TestCase): |
149
|
|
|
""" |
150
|
|
|
@todo: Switch to pytest to get the assertEqual readout from assert in |
151
|
|
|
bare functions. |
152
|
|
|
""" |
153
|
|
|
def test_powerset(self): |
154
|
|
|
"""Test that powerset() behaves as expected""" |
155
|
|
|
src_set = (1, 2, 3) |
156
|
|
|
expected = [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] |
157
|
|
|
|
158
|
|
|
for test_set in (tuple(src_set), list(src_set), set(src_set)): |
159
|
|
|
result = list(powerset(test_set)) |
160
|
|
|
|
161
|
|
|
# Obvious requirements |
162
|
|
|
self.assertIn(tuple(), result) |
163
|
|
|
self.assertIn(tuple(test_set), result) |
164
|
|
|
|
165
|
|
|
# Check that only subsets are returned |
166
|
|
|
for subset in expected: |
167
|
|
|
for item in subset: |
168
|
|
|
self.assertIn(item, test_set) |
169
|
|
|
|
170
|
|
|
# Check that ALL subsets are returned |
171
|
|
|
# FIXME: This shouldn't enforce an ordering constraint. |
172
|
|
|
self.assertEqual(list(powerset([1, 2, 3])), expected) |
173
|
|
|
|
174
|
|
|
# TODO: Test fmt_table |
175
|
|
|
|
176
|
|
|
# TODO: Test _make_positions |
177
|
|
|
|
178
|
|
|
def test_xiniterror_str(self): |
179
|
|
|
"""XInitError.__str__ output contains provided text""" |
180
|
|
|
self.assertIn("Testing 123", XInitError("Testing 123")) |
181
|
|
|
|
182
|
|
|
class TestWindowGravity(unittest.TestCase): |
183
|
|
|
"""Test the equivalence and correctness of L{wm.GRAVITY} values.""" |
184
|
|
|
|
185
|
|
|
def setUp(self): |
186
|
|
|
# Set up a nice, oddly-shaped fake desktop made from screens |
187
|
|
|
# I actually have access to (though not all on the same PC) |
188
|
|
|
self.screens = [ |
189
|
|
|
gtk.gdk.Rectangle(0, 0, 1280, 1024), |
190
|
|
|
gtk.gdk.Rectangle(1280, 0, 1280, 1024), |
191
|
|
|
gtk.gdk.Rectangle(0, 1024, 1680, 1050), |
192
|
|
|
gtk.gdk.Rectangle(1680, 1024, 1440, 900) |
193
|
|
|
] |
194
|
|
|
|
195
|
|
|
# TODO: Also work in some fake panel struts |
196
|
|
|
self.desktop = gtk.gdk.Region() |
197
|
|
|
for rect in self.screens: |
198
|
|
|
self.desktop.union_with_rect(rect) |
199
|
|
|
|
200
|
|
|
def test_gravity_equivalence(self): |
201
|
|
|
"""Gravity Lookup Table: text/GDK/WNCK constants are equivalent""" |
202
|
|
|
for alignment in ('CENTER', 'NORTH', 'NORTH_WEST', 'SOUTH_EAST', |
203
|
|
|
'EAST', 'NORTH_EAST', 'SOUTH', 'SOUTH_WEST', 'WEST'): |
204
|
|
|
self.assertEqual(wm.GRAVITY[alignment], |
205
|
|
|
wm.GRAVITY[getattr(gtk.gdk, 'GRAVITY_{}'.format(alignment))]) |
206
|
|
|
self.assertEqual( |
207
|
|
|
wm.GRAVITY[getattr(wnck, 'WINDOW_GRAVITY_{}'.format( |
208
|
|
|
alignment.replace('_', '')))], |
209
|
|
|
wm.GRAVITY[getattr(gtk.gdk, 'GRAVITY_{}'.format( |
210
|
|
|
alignment))]) |
211
|
|
|
|
212
|
|
|
def test_gravity_correctness(self): |
213
|
|
|
"""Gravity Lookup Table: Constants have correct percentage values""" |
214
|
|
|
for alignment, coords in ( |
215
|
|
|
('NORTH_WEST', (0, 0)), ('NORTH', (0.5, 0)), |
216
|
|
|
('NORTH_EAST', (1.0, 0.0)), ('WEST', (0.0, 0.5)), |
217
|
|
|
('CENTER', (0.5, 0.5)), ('EAST', (1, 0.5)), |
218
|
|
|
('SOUTH_WEST', (0.0, 1.0)), ('SOUTH', (0.5, 1.0)), |
219
|
|
|
('SOUTH_EAST', (1.0, 1.0))): |
220
|
|
|
self.assertEqual(wm.GRAVITY[ |
221
|
|
|
getattr(gtk.gdk, 'GRAVITY_%s' % alignment)], coords) |
222
|
|
|
|
223
|
|
|
class TestWindowManagerDetached(unittest.TestCase): |
224
|
|
|
"""Tests which exercise L{wm.WindowManager} without needing X11.""" |
225
|
|
|
|
226
|
|
|
def setUp(self): |
227
|
|
|
# Shorthand |
228
|
|
|
self.WM = wm.WindowManager # pylint: disable=invalid-name |
229
|
|
|
|
230
|
|
|
# Set up a nice, oddly-shaped fake desktop made from screens |
231
|
|
|
# I actually have access to (though not all on the same PC) |
232
|
|
|
self.screens = [ |
233
|
|
|
gtk.gdk.Rectangle(0, 0, 1280, 1024), |
234
|
|
|
gtk.gdk.Rectangle(1280, 0, 1280, 1024), |
235
|
|
|
gtk.gdk.Rectangle(0, 1024, 1680, 1050), |
236
|
|
|
gtk.gdk.Rectangle(1680, 1024, 1440, 900) |
237
|
|
|
] |
238
|
|
|
|
239
|
|
|
# TODO: Also work in some fake panel struts |
240
|
|
|
self.desktop = gtk.gdk.Region() |
241
|
|
|
for rect in self.screens: |
242
|
|
|
self.desktop.union_with_rect(rect) |
243
|
|
|
|
244
|
|
|
def test_win_gravity_noop(self): |
245
|
|
|
"""WindowManager.calc_win_gravity: north-west should be a no-op |
246
|
|
|
|
247
|
|
|
(Might as well use the screen shapes to test this. It saves effort.) |
248
|
|
|
""" |
249
|
|
|
for rect in [self.desktop.get_clipbox()] + self.screens: |
250
|
|
|
self.assertEqual((rect.x, rect.y), |
251
|
|
|
self.WM.calc_win_gravity(rect, gtk.gdk.GRAVITY_NORTH_WEST), |
252
|
|
|
"NORTHWEST gravity should be a no-op.") |
253
|
|
|
|
254
|
|
|
def test_win_gravity_results(self): |
255
|
|
|
"""WindowManager.calc_win_gravity: proper results""" |
256
|
|
|
for edge in (100, 200): |
257
|
|
|
ehalf = edge / 2 |
258
|
|
|
for gravity, expect in ( |
259
|
|
|
('NORTH_WEST', (0, 0)), ('NORTH', (-ehalf, 0)), |
260
|
|
|
('NORTH_EAST', (-edge, 0)), ('WEST', (0, -ehalf)), |
261
|
|
|
('CENTER', (-ehalf, -ehalf)), ('EAST', (-edge, -ehalf)), |
262
|
|
|
('SOUTH_WEST', (0, -edge)), ('SOUTH', (-ehalf, -edge)), |
263
|
|
|
('SOUTH_EAST', (-edge, -edge))): |
264
|
|
|
rect = gtk.gdk.Rectangle(0, 0, edge, edge) |
265
|
|
|
grav = getattr(gtk.gdk, 'GRAVITY_%s' % gravity) |
266
|
|
|
|
267
|
|
|
self.assertEqual(self.WM.calc_win_gravity(rect, grav), expect) |
268
|
|
|
|
269
|
|
|
# TODO: Test the rest of the functionality |
270
|
|
|
|
271
|
|
|
# vim: set sw=4 sts=4 expandtab : |
272
|
|
|
|