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