1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16import datetime
17import locale
18import os
19
20import mock
21
22from twisted.internet import reactor
23from twisted.internet import task
24from twisted.trial import unittest
25
26from buildbot import util
27
28
29class formatInterval(unittest.TestCase):
30
31    def test_zero(self):
32        self.assertEqual(util.formatInterval(0), "0 secs")
33
34    def test_seconds_singular(self):
35        self.assertEqual(util.formatInterval(1), "1 secs")
36
37    def test_seconds(self):
38        self.assertEqual(util.formatInterval(7), "7 secs")
39
40    def test_minutes_one(self):
41        self.assertEqual(util.formatInterval(60), "60 secs")
42
43    def test_minutes_over_one(self):
44        self.assertEqual(util.formatInterval(61), "1 mins, 1 secs")
45
46    def test_minutes(self):
47        self.assertEqual(util.formatInterval(300), "5 mins, 0 secs")
48
49    def test_hours_one(self):
50        self.assertEqual(util.formatInterval(3600), "60 mins, 0 secs")
51
52    def test_hours_over_one_sec(self):
53        self.assertEqual(util.formatInterval(3601), "1 hrs, 1 secs")
54
55    def test_hours_over_one_min(self):
56        self.assertEqual(util.formatInterval(3660), "1 hrs, 60 secs")
57
58    def test_hours(self):
59        self.assertEqual(util.formatInterval(7200), "2 hrs, 0 secs")
60
61    def test_mixed(self):
62        self.assertEqual(util.formatInterval(7392), "2 hrs, 3 mins, 12 secs")
63
64
65class TestHumanReadableDelta(unittest.TestCase):
66
67    def test_timeDeltaToHumanReadable(self):
68        """
69        It will return a human readable time difference.
70        """
71        try:
72            datetime.datetime.fromtimestamp(1)
73        except OSError as e:
74            raise unittest.SkipTest(
75                "Python 3.6 bug on Windows: "
76                "https://bugs.python.org/issue29097") from e
77        result = util.human_readable_delta(1, 1)
78        self.assertEqual('super fast', result)
79
80        result = util.human_readable_delta(1, 2)
81        self.assertEqual('1 seconds', result)
82
83        result = util.human_readable_delta(1, 61)
84        self.assertEqual('1 minutes', result)
85
86        result = util.human_readable_delta(1, 62)
87        self.assertEqual('1 minutes, 1 seconds', result)
88
89        result = util.human_readable_delta(1, 60 * 60 + 1)
90        self.assertEqual('1 hours', result)
91
92        result = util.human_readable_delta(1, 60 * 60 + 61)
93        self.assertEqual('1 hours, 1 minutes', result)
94
95        result = util.human_readable_delta(1, 60 * 60 + 62)
96        self.assertEqual('1 hours, 1 minutes, 1 seconds', result)
97
98        result = util.human_readable_delta(1, 24 * 60 * 60 + 1)
99        self.assertEqual('1 days', result)
100
101        result = util.human_readable_delta(1, 24 * 60 * 60 + 2)
102        self.assertEqual('1 days, 1 seconds', result)
103
104
105class TestFuzzyInterval(unittest.TestCase):
106
107    def test_moment(self):
108        self.assertEqual(util.fuzzyInterval(1), "a moment")
109
110    def test_seconds(self):
111        self.assertEqual(util.fuzzyInterval(17), "17 seconds")
112
113    def test_seconds_rounded(self):
114        self.assertEqual(util.fuzzyInterval(48), "50 seconds")
115
116    def test_minute(self):
117        self.assertEqual(util.fuzzyInterval(58), "a minute")
118
119    def test_minutes(self):
120        self.assertEqual(util.fuzzyInterval(3 * 60 + 24), "3 minutes")
121
122    def test_minutes_rounded(self):
123        self.assertEqual(util.fuzzyInterval(32 * 60 + 24), "30 minutes")
124
125    def test_hour(self):
126        self.assertEqual(util.fuzzyInterval(3600 + 1200), "an hour")
127
128    def test_hours(self):
129        self.assertEqual(util.fuzzyInterval(9 * 3600 - 720), "9 hours")
130
131    def test_day(self):
132        self.assertEqual(util.fuzzyInterval(32 * 3600 + 124), "a day")
133
134    def test_days(self):
135        self.assertEqual(util.fuzzyInterval((19 + 24) * 3600 + 124), "2 days")
136
137    def test_month(self):
138        self.assertEqual(util.fuzzyInterval(36 * 24 * 3600 + 124), "a month")
139
140    def test_months(self):
141        self.assertEqual(util.fuzzyInterval(86 * 24 * 3600 + 124), "3 months")
142
143    def test_year(self):
144        self.assertEqual(util.fuzzyInterval(370 * 24 * 3600), "a year")
145
146    def test_years(self):
147        self.assertEqual(util.fuzzyInterval((2 * 365 + 96) * 24 * 3600), "2 years")
148
149
150class safeTranslate(unittest.TestCase):
151
152    def test_str_good(self):
153        self.assertEqual(util.safeTranslate(str("full")), b"full")
154
155    def test_str_bad(self):
156        self.assertEqual(util.safeTranslate(str("speed=slow;quality=high")),
157                         b"speed_slow_quality_high")
158
159    def test_str_pathological(self):
160        # if you needed proof this wasn't for use with sensitive data
161        self.assertEqual(util.safeTranslate(str("p\ath\x01ogy")),
162                         b"p\ath\x01ogy")  # bad chars still here!
163
164    def test_unicode_good(self):
165        self.assertEqual(util.safeTranslate("full"), b"full")
166
167    def test_unicode_bad(self):
168        self.assertEqual(util.safeTranslate(str("speed=slow;quality=high")),
169                         b"speed_slow_quality_high")
170
171    def test_unicode_pathological(self):
172        self.assertEqual(util.safeTranslate("\u0109"),
173                         b"\xc4\x89")  # yuck!
174
175
176class naturalSort(unittest.TestCase):
177
178    def test_alpha(self):
179        self.assertEqual(
180            util.naturalSort(['x', 'aa', 'ab']),
181            ['aa', 'ab', 'x'])
182
183    def test_numeric(self):
184        self.assertEqual(
185            util.naturalSort(['1', '10', '11', '2', '20']),
186            ['1', '2', '10', '11', '20'])
187
188    def test_alphanum(self):
189        l1 = 'aa10ab aa1ab aa10aa f a aa3 aa30 aa3a aa30a'.split()
190        l2 = 'a aa1ab aa3 aa3a aa10aa aa10ab aa30 aa30a f'.split()
191        self.assertEqual(util.naturalSort(l1), l2)
192
193
194class none_or_str(unittest.TestCase):
195
196    def test_none(self):
197        self.assertEqual(util.none_or_str(None), None)
198
199    def test_str(self):
200        self.assertEqual(util.none_or_str("hi"), "hi")
201
202    def test_int(self):
203        self.assertEqual(util.none_or_str(199), "199")
204
205
206class TimeFunctions(unittest.TestCase):
207
208    def test_UTC(self):
209        self.assertEqual(util.UTC.utcoffset(datetime.datetime.now()),
210                         datetime.timedelta(0))
211        self.assertEqual(util.UTC.dst(datetime.datetime.now()),
212                         datetime.timedelta(0))
213        self.assertEqual(util.UTC.tzname(datetime.datetime.utcnow()), "UTC")
214
215    def test_epoch2datetime(self):
216        self.assertEqual(util.epoch2datetime(0),
217                         datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=util.UTC))
218        self.assertEqual(util.epoch2datetime(1300000000),
219                         datetime.datetime(2011, 3, 13, 7, 6, 40, tzinfo=util.UTC))
220
221    def test_datetime2epoch(self):
222        dt = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=util.UTC)
223        self.assertEqual(util.datetime2epoch(dt), 0)
224        dt = datetime.datetime(2011, 3, 13, 7, 6, 40, tzinfo=util.UTC)
225        self.assertEqual(util.datetime2epoch(dt), 1300000000)
226
227
228class DiffSets(unittest.TestCase):
229
230    def test_empty(self):
231        removed, added = util.diffSets(set([]), set([]))
232        self.assertEqual((removed, added), (set([]), set([])))
233
234    def test_no_lists(self):
235        removed, added = util.diffSets([1, 2], [2, 3])
236        self.assertEqual((removed, added), (set([1]), set([3])))
237
238    def test_no_overlap(self):
239        removed, added = util.diffSets(set([1, 2]), set([3, 4]))
240        self.assertEqual((removed, added), (set([1, 2]), set([3, 4])))
241
242    def test_no_change(self):
243        removed, added = util.diffSets(set([1, 2]), set([1, 2]))
244        self.assertEqual((removed, added), (set([]), set([])))
245
246    def test_added(self):
247        removed, added = util.diffSets(set([1, 2]), set([1, 2, 3]))
248        self.assertEqual((removed, added), (set([]), set([3])))
249
250    def test_removed(self):
251        removed, added = util.diffSets(set([1, 2]), set([1]))
252        self.assertEqual((removed, added), (set([2]), set([])))
253
254
255class MakeList(unittest.TestCase):
256
257    def test_empty_string(self):
258        self.assertEqual(util.makeList(''), [''])
259
260    def test_None(self):
261        self.assertEqual(util.makeList(None), [])
262
263    def test_string(self):
264        self.assertEqual(util.makeList('hello'), ['hello'])
265
266    def test_unicode(self):
267        self.assertEqual(util.makeList('\N{SNOWMAN}'), ['\N{SNOWMAN}'])
268
269    def test_list(self):
270        self.assertEqual(util.makeList(['a', 'b']), ['a', 'b'])
271
272    def test_tuple(self):
273        self.assertEqual(util.makeList(('a', 'b')), ['a', 'b'])
274
275    def test_copy(self):
276        input = ['a', 'b']
277        output = util.makeList(input)
278        input.append('c')
279        self.assertEqual(output, ['a', 'b'])
280
281
282class Flatten(unittest.TestCase):
283
284    def test_simple(self):
285        self.assertEqual(util.flatten([1, 2, 3]), [1, 2, 3])
286
287    def test_deep(self):
288        self.assertEqual(util.flatten([[1, 2], 3, [[4]]]),
289                         [1, 2, 3, 4])
290
291    # def test_deeply_nested(self):
292    #     self.assertEqual(util.flatten([5, [6, (7, 8)]]),
293    #                      [5, 6, 7, 8])
294
295    # def test_tuples(self):
296    #     self.assertEqual(util.flatten([(1, 2), 3]), [1, 2, 3])
297
298    def test_dict(self):
299        d = {'a': [5, 6, 7], 'b': [7, 8, 9]}
300        self.assertEqual(util.flatten(d), d)
301
302    def test_string(self):
303        self.assertEqual(util.flatten("abc"), "abc")
304
305
306class Ascii2Unicode(unittest.TestCase):
307
308    def test_unicode(self):
309        rv = util.bytes2unicode('\N{SNOWMAN}', encoding='ascii')
310        self.assertEqual((rv, type(rv)), ('\N{SNOWMAN}', str))
311
312    def test_ascii(self):
313        rv = util.bytes2unicode('abcd', encoding='ascii')
314        self.assertEqual((rv, type(rv)), ('abcd', str))
315
316    def test_nonascii(self):
317        with self.assertRaises(UnicodeDecodeError):
318            util.bytes2unicode(b'a\x85', encoding='ascii')
319
320    def test_None(self):
321        self.assertEqual(util.bytes2unicode(None, encoding='ascii'), None)
322
323    def test_bytes2unicode(self):
324        rv1 = util.bytes2unicode(b'abcd')
325        rv2 = util.bytes2unicode('efgh')
326
327        self.assertEqual(type(rv1), str)
328        self.assertEqual(type(rv2), str)
329
330
331class StringToBoolean(unittest.TestCase):
332
333    def test_it(self):
334        stringValues = [
335            (b'on', True),
336            (b'true', True),
337            (b'yes', True),
338            (b'1', True),
339            (b'off', False),
340            (b'false', False),
341            (b'no', False),
342            (b'0', False),
343            (b'ON', True),
344            (b'TRUE', True),
345            (b'YES', True),
346            (b'OFF', False),
347            (b'FALSE', False),
348            (b'NO', False),
349        ]
350        for s, b in stringValues:
351            self.assertEqual(util.string2boolean(s), b, repr(s))
352
353    def test_ascii(self):
354        rv = util.bytes2unicode(b'abcd', encoding='ascii')
355        self.assertEqual((rv, type(rv)), ('abcd', str))
356
357    def test_nonascii(self):
358        with self.assertRaises(UnicodeDecodeError):
359            util.bytes2unicode(b'a\x85', encoding='ascii')
360
361    def test_None(self):
362        self.assertEqual(util.bytes2unicode(None, encoding='ascii'), None)
363
364
365class AsyncSleep(unittest.TestCase):
366
367    def test_sleep(self):
368        clock = task.Clock()
369        self.patch(reactor, 'callLater', clock.callLater)
370        d = util.asyncSleep(2)
371        self.assertFalse(d.called)
372        clock.advance(1)
373        self.assertFalse(d.called)
374        clock.advance(1)
375        self.assertTrue(d.called)
376
377
378class FunctionalEnvironment(unittest.TestCase):
379
380    def test_working_locale(self):
381        environ = {'LANG': 'en_GB.UTF-8'}
382        self.patch(os, 'environ', environ)
383        config = mock.Mock()
384        util.check_functional_environment(config)
385        self.assertEqual(config.error.called, False)
386
387    def test_broken_locale(self):
388        def err():
389            raise KeyError
390        self.patch(locale, 'getdefaultlocale', err)
391        config = mock.Mock()
392        util.check_functional_environment(config)
393        config.error.assert_called_with(mock.ANY)
394
395
396class StripUrlPassword(unittest.TestCase):
397
398    def test_simple_url(self):
399        self.assertEqual(util.stripUrlPassword('http://foo.com/bar'),
400                         'http://foo.com/bar')
401
402    def test_username(self):
403        self.assertEqual(util.stripUrlPassword('http://d@foo.com/bar'),
404                         'http://d@foo.com/bar')
405
406    def test_username_with_at(self):
407        self.assertEqual(util.stripUrlPassword('http://d@bb.net@foo.com/bar'),
408                         'http://d@bb.net@foo.com/bar')
409
410    def test_username_pass(self):
411        self.assertEqual(util.stripUrlPassword('http://d:secret@foo.com/bar'),
412                         'http://d:xxxx@foo.com/bar')
413
414    def test_username_pass_with_at(self):
415        self.assertEqual(
416            util.stripUrlPassword('http://d@bb.net:scrt@foo.com/bar'),
417            'http://d@bb.net:xxxx@foo.com/bar')
418
419
420class JoinList(unittest.TestCase):
421
422    def test_list(self):
423        self.assertEqual(util.join_list(['aa', 'bb']), 'aa bb')
424
425    def test_tuple(self):
426        self.assertEqual(util.join_list(('aa', 'bb')), 'aa bb')
427
428    def test_string(self):
429        self.assertEqual(util.join_list('abc'), 'abc')
430
431    def test_unicode(self):
432        self.assertEqual(util.join_list('abc'), 'abc')
433
434    def test_nonascii(self):
435        with self.assertRaises(UnicodeDecodeError):
436            util.join_list([b'\xff'])
437
438
439class CommandToString(unittest.TestCase):
440
441    def test_short_string(self):
442        self.assertEqual(util.command_to_string("ab cd"), "'ab cd'")
443
444    def test_long_string(self):
445        self.assertEqual(util.command_to_string("ab cd ef"), "'ab cd ...'")
446
447    def test_list(self):
448        self.assertEqual(util.command_to_string(['ab', 'cd', 'ef']),
449                         "'ab cd ...'")
450
451    def test_nested_list(self):
452        self.assertEqual(util.command_to_string(['ab', ['cd', ['ef']]]),
453                         "'ab cd ...'")
454
455    def test_object(self):
456        # this looks like a renderable
457        self.assertEqual(util.command_to_string(object()), None)
458
459    def test_list_with_objects(self):
460        # the object looks like a renderable, and is skipped
461        self.assertEqual(util.command_to_string(['ab', object(), 'cd']),
462                         "'ab cd'")
463
464    def test_invalid_ascii(self):
465        self.assertEqual(util.command_to_string(b'a\xffc'), "'a\ufffdc'")
466
467
468class TestRewrap(unittest.TestCase):
469
470    def test_main(self):
471        tests = [
472            ("", "", None),
473            ("\n", "\n", None),
474            ("\n  ", "\n", None),
475            ("  \n", "\n", None),
476            ("  \n  ", "\n", None),
477            ("""
478                multiline
479                with
480                indent
481                """,
482             "\nmultiline with indent",
483             None),
484            ("""\
485                multiline
486                with
487                indent
488
489                """,
490             "multiline with indent\n",
491             None),
492            ("""\
493                 multiline
494                 with
495                 indent
496
497                 """,
498             "multiline with indent\n",
499             None),
500            ("""\
501                multiline
502                with
503                indent
504
505                  and
506                   formatting
507                """,
508             "multiline with indent\n  and\n   formatting\n",
509             None),
510            ("""\
511                multiline
512                with
513                indent
514                and wrapping
515
516                  and
517                   formatting
518                """,
519             "multiline with\nindent and\nwrapping\n  and\n   formatting\n",
520             15),
521        ]
522
523        for text, expected, width in tests:
524            self.assertEqual(util.rewrap(text, width=width), expected)
525
526
527class TestMerge(unittest.TestCase):
528
529    def test_merge(self):
530        self.assertEqual(
531            util.dictionary_merge(
532                {
533                    'a': {'b': 1}
534                },
535                {
536                    'a': {'c': 2}
537                }),
538            {
539                'a': {'b': 1, 'c': 2}
540            })
541
542    def test_overwrite(self):
543        self.assertEqual(
544            util.dictionary_merge(
545                {
546                    'a': {'b': 1}
547                },
548                {
549                    'a': 1
550                }),
551            {
552                'a': 1
553            })
554
555    def test_overwrite2(self):
556        self.assertEqual(
557            util.dictionary_merge(
558                {
559                    'a': {'b': 1, 'c': 2}
560                },
561                {
562                    'a': {'b': [1, 2, 3]}
563                }),
564            {
565                'a': {'b': [1, 2, 3], 'c': 2}
566            })
567