##// END OF EJS Templates
urls: improve removal of credentials in repository header....
ergo -
r4399:326ccc76 stable
parent child Browse files
Show More
@@ -1,1104 +1,1070 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 37 import getpass
38 38 from functools import update_wrapper, partial
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import sqlalchemy.exc
44 44 import sqlalchemy.sql
45 45 import webob
46 46 import pyramid.threadlocal
47 47 from pyramid import compat
48 48 from pyramid.settings import asbool
49 49
50 50 import rhodecode
51 51 from rhodecode.translation import _, _pluralize
52 52
53 53
54 54 def md5(s):
55 55 return hashlib.md5(s).hexdigest()
56 56
57 57
58 58 def md5_safe(s):
59 59 return md5(safe_str(s))
60 60
61 61
62 62 def sha1(s):
63 63 return hashlib.sha1(s).hexdigest()
64 64
65 65
66 66 def sha1_safe(s):
67 67 return sha1(safe_str(s))
68 68
69 69
70 70 def __get_lem(extra_mapping=None):
71 71 """
72 72 Get language extension map based on what's inside pygments lexers
73 73 """
74 74 d = collections.defaultdict(lambda: [])
75 75
76 76 def __clean(s):
77 77 s = s.lstrip('*')
78 78 s = s.lstrip('.')
79 79
80 80 if s.find('[') != -1:
81 81 exts = []
82 82 start, stop = s.find('['), s.find(']')
83 83
84 84 for suffix in s[start + 1:stop]:
85 85 exts.append(s[:s.find('[')] + suffix)
86 86 return [e.lower() for e in exts]
87 87 else:
88 88 return [s.lower()]
89 89
90 90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 91 m = map(__clean, t[-2])
92 92 if m:
93 93 m = reduce(lambda x, y: x + y, m)
94 94 for ext in m:
95 95 desc = lx.replace('Lexer', '')
96 96 d[ext].append(desc)
97 97
98 98 data = dict(d)
99 99
100 100 extra_mapping = extra_mapping or {}
101 101 if extra_mapping:
102 102 for k, v in extra_mapping.items():
103 103 if k not in data:
104 104 # register new mapping2lexer
105 105 data[k] = [v]
106 106
107 107 return data
108 108
109 109
110 110 def str2bool(_str):
111 111 """
112 112 returns True/False value from given string, it tries to translate the
113 113 string into boolean
114 114
115 115 :param _str: string value to translate into boolean
116 116 :rtype: boolean
117 117 :returns: boolean from given string
118 118 """
119 119 if _str is None:
120 120 return False
121 121 if _str in (True, False):
122 122 return _str
123 123 _str = str(_str).strip().lower()
124 124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125 125
126 126
127 127 def aslist(obj, sep=None, strip=True):
128 128 """
129 129 Returns given string separated by sep as list
130 130
131 131 :param obj:
132 132 :param sep:
133 133 :param strip:
134 134 """
135 135 if isinstance(obj, (basestring,)):
136 136 lst = obj.split(sep)
137 137 if strip:
138 138 lst = [v.strip() for v in lst]
139 139 return lst
140 140 elif isinstance(obj, (list, tuple)):
141 141 return obj
142 142 elif obj is None:
143 143 return []
144 144 else:
145 145 return [obj]
146 146
147 147
148 148 def convert_line_endings(line, mode):
149 149 """
150 150 Converts a given line "line end" accordingly to given mode
151 151
152 152 Available modes are::
153 153 0 - Unix
154 154 1 - Mac
155 155 2 - DOS
156 156
157 157 :param line: given line to convert
158 158 :param mode: mode to convert to
159 159 :rtype: str
160 160 :return: converted line according to mode
161 161 """
162 162 if mode == 0:
163 163 line = line.replace('\r\n', '\n')
164 164 line = line.replace('\r', '\n')
165 165 elif mode == 1:
166 166 line = line.replace('\r\n', '\r')
167 167 line = line.replace('\n', '\r')
168 168 elif mode == 2:
169 169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 170 return line
171 171
172 172
173 173 def detect_mode(line, default):
174 174 """
175 175 Detects line break for given line, if line break couldn't be found
176 176 given default value is returned
177 177
178 178 :param line: str line
179 179 :param default: default
180 180 :rtype: int
181 181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 182 """
183 183 if line.endswith('\r\n'):
184 184 return 2
185 185 elif line.endswith('\n'):
186 186 return 0
187 187 elif line.endswith('\r'):
188 188 return 1
189 189 else:
190 190 return default
191 191
192 192
193 193 def safe_int(val, default=None):
194 194 """
195 195 Returns int() of val if val is not convertable to int use default
196 196 instead
197 197
198 198 :param val:
199 199 :param default:
200 200 """
201 201
202 202 try:
203 203 val = int(val)
204 204 except (ValueError, TypeError):
205 205 val = default
206 206
207 207 return val
208 208
209 209
210 210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
211 211 """
212 212 safe unicode function. Does few trick to turn str_ into unicode
213 213
214 214 In case of UnicodeDecode error, we try to return it with encoding detected
215 215 by chardet library if it fails fallback to unicode with errors replaced
216 216
217 217 :param str_: string to decode
218 218 :rtype: unicode
219 219 :returns: unicode object
220 220 """
221 221 if isinstance(str_, unicode):
222 222 return str_
223 223
224 224 if not from_encoding:
225 225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 226 'utf8'), sep=',')
227 227 from_encoding = DEFAULT_ENCODINGS
228 228
229 229 if not isinstance(from_encoding, (list, tuple)):
230 230 from_encoding = [from_encoding]
231 231
232 232 try:
233 233 return unicode(str_)
234 234 except UnicodeDecodeError:
235 235 pass
236 236
237 237 for enc in from_encoding:
238 238 try:
239 239 return unicode(str_, enc)
240 240 except UnicodeDecodeError:
241 241 pass
242 242
243 243 if use_chardet:
244 244 try:
245 245 import chardet
246 246 encoding = chardet.detect(str_)['encoding']
247 247 if encoding is None:
248 248 raise Exception()
249 249 return str_.decode(encoding)
250 250 except (ImportError, UnicodeDecodeError, Exception):
251 251 return unicode(str_, from_encoding[0], 'replace')
252 252 else:
253 253 return unicode(str_, from_encoding[0], 'replace')
254 254
255 255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
256 256 """
257 257 safe str function. Does few trick to turn unicode_ into string
258 258
259 259 In case of UnicodeEncodeError, we try to return it with encoding detected
260 260 by chardet library if it fails fallback to string with errors replaced
261 261
262 262 :param unicode_: unicode to encode
263 263 :rtype: str
264 264 :returns: str object
265 265 """
266 266
267 267 # if it's not basestr cast to str
268 268 if not isinstance(unicode_, compat.string_types):
269 269 return str(unicode_)
270 270
271 271 if isinstance(unicode_, str):
272 272 return unicode_
273 273
274 274 if not to_encoding:
275 275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
276 276 'utf8'), sep=',')
277 277 to_encoding = DEFAULT_ENCODINGS
278 278
279 279 if not isinstance(to_encoding, (list, tuple)):
280 280 to_encoding = [to_encoding]
281 281
282 282 for enc in to_encoding:
283 283 try:
284 284 return unicode_.encode(enc)
285 285 except UnicodeEncodeError:
286 286 pass
287 287
288 288 if use_chardet:
289 289 try:
290 290 import chardet
291 291 encoding = chardet.detect(unicode_)['encoding']
292 292 if encoding is None:
293 293 raise UnicodeEncodeError()
294 294
295 295 return unicode_.encode(encoding)
296 296 except (ImportError, UnicodeEncodeError):
297 297 return unicode_.encode(to_encoding[0], 'replace')
298 298 else:
299 299 return unicode_.encode(to_encoding[0], 'replace')
300 300
301 301
302 302 def remove_suffix(s, suffix):
303 303 if s.endswith(suffix):
304 304 s = s[:-1 * len(suffix)]
305 305 return s
306 306
307 307
308 308 def remove_prefix(s, prefix):
309 309 if s.startswith(prefix):
310 310 s = s[len(prefix):]
311 311 return s
312 312
313 313
314 314 def find_calling_context(ignore_modules=None):
315 315 """
316 316 Look through the calling stack and return the frame which called
317 317 this function and is part of core module ( ie. rhodecode.* )
318 318
319 319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
320 320 """
321 321
322 322 ignore_modules = ignore_modules or []
323 323
324 324 f = sys._getframe(2)
325 325 while f.f_back is not None:
326 326 name = f.f_globals.get('__name__')
327 327 if name and name.startswith(__name__.split('.')[0]):
328 328 if name not in ignore_modules:
329 329 return f
330 330 f = f.f_back
331 331 return None
332 332
333 333
334 334 def ping_connection(connection, branch):
335 335 if branch:
336 336 # "branch" refers to a sub-connection of a connection,
337 337 # we don't want to bother pinging on these.
338 338 return
339 339
340 340 # turn off "close with result". This flag is only used with
341 341 # "connectionless" execution, otherwise will be False in any case
342 342 save_should_close_with_result = connection.should_close_with_result
343 343 connection.should_close_with_result = False
344 344
345 345 try:
346 346 # run a SELECT 1. use a core select() so that
347 347 # the SELECT of a scalar value without a table is
348 348 # appropriately formatted for the backend
349 349 connection.scalar(sqlalchemy.sql.select([1]))
350 350 except sqlalchemy.exc.DBAPIError as err:
351 351 # catch SQLAlchemy's DBAPIError, which is a wrapper
352 352 # for the DBAPI's exception. It includes a .connection_invalidated
353 353 # attribute which specifies if this connection is a "disconnect"
354 354 # condition, which is based on inspection of the original exception
355 355 # by the dialect in use.
356 356 if err.connection_invalidated:
357 357 # run the same SELECT again - the connection will re-validate
358 358 # itself and establish a new connection. The disconnect detection
359 359 # here also causes the whole connection pool to be invalidated
360 360 # so that all stale connections are discarded.
361 361 connection.scalar(sqlalchemy.sql.select([1]))
362 362 else:
363 363 raise
364 364 finally:
365 365 # restore "close with result"
366 366 connection.should_close_with_result = save_should_close_with_result
367 367
368 368
369 369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
370 370 """Custom engine_from_config functions."""
371 371 log = logging.getLogger('sqlalchemy.engine')
372 372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
373 373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
374 374
375 375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
376 376
377 377 def color_sql(sql):
378 378 color_seq = '\033[1;33m' # This is yellow: code 33
379 379 normal = '\x1b[0m'
380 380 return ''.join([color_seq, sql, normal])
381 381
382 382 if use_ping_connection:
383 383 log.debug('Adding ping_connection on the engine config.')
384 384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
385 385
386 386 if debug:
387 387 # attach events only for debug configuration
388 388 def before_cursor_execute(conn, cursor, statement,
389 389 parameters, context, executemany):
390 390 setattr(conn, 'query_start_time', time.time())
391 391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
392 392 calling_context = find_calling_context(ignore_modules=[
393 393 'rhodecode.lib.caching_query',
394 394 'rhodecode.model.settings',
395 395 ])
396 396 if calling_context:
397 397 log.info(color_sql('call context %s:%s' % (
398 398 calling_context.f_code.co_filename,
399 399 calling_context.f_lineno,
400 400 )))
401 401
402 402 def after_cursor_execute(conn, cursor, statement,
403 403 parameters, context, executemany):
404 404 delattr(conn, 'query_start_time')
405 405
406 406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
407 407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
408 408
409 409 return engine
410 410
411 411
412 412 def get_encryption_key(config):
413 413 secret = config.get('rhodecode.encrypted_values.secret')
414 414 default = config['beaker.session.secret']
415 415 return secret or default
416 416
417 417
418 418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
419 419 short_format=False):
420 420 """
421 421 Turns a datetime into an age string.
422 422 If show_short_version is True, this generates a shorter string with
423 423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
424 424
425 425 * IMPORTANT*
426 426 Code of this function is written in special way so it's easier to
427 427 backport it to javascript. If you mean to update it, please also update
428 428 `jquery.timeago-extension.js` file
429 429
430 430 :param prevdate: datetime object
431 431 :param now: get current time, if not define we use
432 432 `datetime.datetime.now()`
433 433 :param show_short_version: if it should approximate the date and
434 434 return a shorter string
435 435 :param show_suffix:
436 436 :param short_format: show short format, eg 2D instead of 2 days
437 437 :rtype: unicode
438 438 :returns: unicode words describing age
439 439 """
440 440
441 441 def _get_relative_delta(now, prevdate):
442 442 base = dateutil.relativedelta.relativedelta(now, prevdate)
443 443 return {
444 444 'year': base.years,
445 445 'month': base.months,
446 446 'day': base.days,
447 447 'hour': base.hours,
448 448 'minute': base.minutes,
449 449 'second': base.seconds,
450 450 }
451 451
452 452 def _is_leap_year(year):
453 453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
454 454
455 455 def get_month(prevdate):
456 456 return prevdate.month
457 457
458 458 def get_year(prevdate):
459 459 return prevdate.year
460 460
461 461 now = now or datetime.datetime.now()
462 462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
463 463 deltas = {}
464 464 future = False
465 465
466 466 if prevdate > now:
467 467 now_old = now
468 468 now = prevdate
469 469 prevdate = now_old
470 470 future = True
471 471 if future:
472 472 prevdate = prevdate.replace(microsecond=0)
473 473 # Get date parts deltas
474 474 for part in order:
475 475 rel_delta = _get_relative_delta(now, prevdate)
476 476 deltas[part] = rel_delta[part]
477 477
478 478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
479 479 # not 1 hour, -59 minutes and -59 seconds)
480 480 offsets = [[5, 60], [4, 60], [3, 24]]
481 481 for element in offsets: # seconds, minutes, hours
482 482 num = element[0]
483 483 length = element[1]
484 484
485 485 part = order[num]
486 486 carry_part = order[num - 1]
487 487
488 488 if deltas[part] < 0:
489 489 deltas[part] += length
490 490 deltas[carry_part] -= 1
491 491
492 492 # Same thing for days except that the increment depends on the (variable)
493 493 # number of days in the month
494 494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
495 495 if deltas['day'] < 0:
496 496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
497 497 deltas['day'] += 29
498 498 else:
499 499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
500 500
501 501 deltas['month'] -= 1
502 502
503 503 if deltas['month'] < 0:
504 504 deltas['month'] += 12
505 505 deltas['year'] -= 1
506 506
507 507 # Format the result
508 508 if short_format:
509 509 fmt_funcs = {
510 510 'year': lambda d: u'%dy' % d,
511 511 'month': lambda d: u'%dm' % d,
512 512 'day': lambda d: u'%dd' % d,
513 513 'hour': lambda d: u'%dh' % d,
514 514 'minute': lambda d: u'%dmin' % d,
515 515 'second': lambda d: u'%dsec' % d,
516 516 }
517 517 else:
518 518 fmt_funcs = {
519 519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
520 520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
521 521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
522 522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
523 523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
524 524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
525 525 }
526 526
527 527 i = 0
528 528 for part in order:
529 529 value = deltas[part]
530 530 if value != 0:
531 531
532 532 if i < 5:
533 533 sub_part = order[i + 1]
534 534 sub_value = deltas[sub_part]
535 535 else:
536 536 sub_value = 0
537 537
538 538 if sub_value == 0 or show_short_version:
539 539 _val = fmt_funcs[part](value)
540 540 if future:
541 541 if show_suffix:
542 542 return _(u'in ${ago}', mapping={'ago': _val})
543 543 else:
544 544 return _(_val)
545 545
546 546 else:
547 547 if show_suffix:
548 548 return _(u'${ago} ago', mapping={'ago': _val})
549 549 else:
550 550 return _(_val)
551 551
552 552 val = fmt_funcs[part](value)
553 553 val_detail = fmt_funcs[sub_part](sub_value)
554 554 mapping = {'val': val, 'detail': val_detail}
555 555
556 556 if short_format:
557 557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
558 558 if show_suffix:
559 559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
560 560 if future:
561 561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
562 562 else:
563 563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
564 564 if show_suffix:
565 565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
566 566 if future:
567 567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
568 568
569 569 return datetime_tmpl
570 570 i += 1
571 571 return _(u'just now')
572 572
573 573
574 574 def age_from_seconds(seconds):
575 575 seconds = safe_int(seconds) or 0
576 576 prevdate = time_to_datetime(time.time() + seconds)
577 577 return age(prevdate, show_suffix=False, show_short_version=True)
578 578
579 579
580 580 def cleaned_uri(uri):
581 581 """
582 582 Quotes '[' and ']' from uri if there is only one of them.
583 583 according to RFC3986 we cannot use such chars in uri
584 584 :param uri:
585 585 :return: uri without this chars
586 586 """
587 587 return urllib.quote(uri, safe='@$:/')
588 588
589 589
590 def uri_filter(uri):
591 """
592 Removes user:password from given url string
593
594 :param uri:
595 :rtype: unicode
596 :returns: filtered list of strings
597 """
598 if not uri:
599 return ''
600
601 proto = ''
602
603 for pat in ('https://', 'http://'):
604 if uri.startswith(pat):
605 uri = uri[len(pat):]
606 proto = pat
607 break
608
609 # remove passwords and username
610 uri = uri[uri.find('@') + 1:]
611
612 # get the port
613 cred_pos = uri.find(':')
614 if cred_pos == -1:
615 host, port = uri, None
616 else:
617 host, port = uri[:cred_pos], uri[cred_pos + 1:]
618
619 return filter(None, [proto, host, port])
620
621
622 590 def credentials_filter(uri):
623 591 """
624 592 Returns a url with removed credentials
625 593
626 594 :param uri:
627 595 """
596 import urlobject
597 url_obj = urlobject.URLObject(cleaned_uri(uri))
598 url_obj = url_obj.without_password().without_username()
628 599
629 uri = uri_filter(uri)
630 # check if we have port
631 if len(uri) > 2 and uri[2]:
632 uri[2] = ':' + uri[2]
633
634 return ''.join(uri)
600 return url_obj
635 601
636 602
637 603 def get_host_info(request):
638 604 """
639 605 Generate host info, to obtain full url e.g https://server.com
640 606 use this
641 607 `{scheme}://{netloc}`
642 608 """
643 609 if not request:
644 610 return {}
645 611
646 612 qualified_home_url = request.route_url('home')
647 613 parsed_url = urlobject.URLObject(qualified_home_url)
648 614 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
649 615
650 616 return {
651 617 'scheme': parsed_url.scheme,
652 618 'netloc': parsed_url.netloc+decoded_path,
653 619 'hostname': parsed_url.hostname,
654 620 }
655 621
656 622
657 623 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
658 624 qualified_home_url = request.route_url('home')
659 625 parsed_url = urlobject.URLObject(qualified_home_url)
660 626 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
661 627
662 628 args = {
663 629 'scheme': parsed_url.scheme,
664 630 'user': '',
665 631 'sys_user': getpass.getuser(),
666 632 # path if we use proxy-prefix
667 633 'netloc': parsed_url.netloc+decoded_path,
668 634 'hostname': parsed_url.hostname,
669 635 'prefix': decoded_path,
670 636 'repo': repo_name,
671 637 'repoid': str(repo_id),
672 638 'repo_type': repo_type
673 639 }
674 640 args.update(override)
675 641 args['user'] = urllib.quote(safe_str(args['user']))
676 642
677 643 for k, v in args.items():
678 644 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
679 645
680 646 # special case for SVN clone url
681 647 if repo_type == 'svn':
682 648 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
683 649
684 650 # remove leading @ sign if it's present. Case of empty user
685 651 url_obj = urlobject.URLObject(uri_tmpl)
686 652 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
687 653
688 654 return safe_unicode(url)
689 655
690 656
691 657 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
692 658 maybe_unreachable=False):
693 659 """
694 660 Safe version of get_commit if this commit doesn't exists for a
695 661 repository it returns a Dummy one instead
696 662
697 663 :param repo: repository instance
698 664 :param commit_id: commit id as str
699 665 :param commit_idx: numeric commit index
700 666 :param pre_load: optional list of commit attributes to load
701 667 :param maybe_unreachable: translate unreachable commits on git repos
702 668 """
703 669 # TODO(skreft): remove these circular imports
704 670 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
705 671 from rhodecode.lib.vcs.exceptions import RepositoryError
706 672 if not isinstance(repo, BaseRepository):
707 673 raise Exception('You must pass an Repository '
708 674 'object as first argument got %s', type(repo))
709 675
710 676 try:
711 677 commit = repo.get_commit(
712 678 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
713 679 maybe_unreachable=maybe_unreachable)
714 680 except (RepositoryError, LookupError):
715 681 commit = EmptyCommit()
716 682 return commit
717 683
718 684
719 685 def datetime_to_time(dt):
720 686 if dt:
721 687 return time.mktime(dt.timetuple())
722 688
723 689
724 690 def time_to_datetime(tm):
725 691 if tm:
726 692 if isinstance(tm, compat.string_types):
727 693 try:
728 694 tm = float(tm)
729 695 except ValueError:
730 696 return
731 697 return datetime.datetime.fromtimestamp(tm)
732 698
733 699
734 700 def time_to_utcdatetime(tm):
735 701 if tm:
736 702 if isinstance(tm, compat.string_types):
737 703 try:
738 704 tm = float(tm)
739 705 except ValueError:
740 706 return
741 707 return datetime.datetime.utcfromtimestamp(tm)
742 708
743 709
744 710 MENTIONS_REGEX = re.compile(
745 711 # ^@ or @ without any special chars in front
746 712 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
747 713 # main body starts with letter, then can be . - _
748 714 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
749 715 re.VERBOSE | re.MULTILINE)
750 716
751 717
752 718 def extract_mentioned_users(s):
753 719 """
754 720 Returns unique usernames from given string s that have @mention
755 721
756 722 :param s: string to get mentions
757 723 """
758 724 usrs = set()
759 725 for username in MENTIONS_REGEX.findall(s):
760 726 usrs.add(username)
761 727
762 728 return sorted(list(usrs), key=lambda k: k.lower())
763 729
764 730
765 731 class AttributeDictBase(dict):
766 732 def __getstate__(self):
767 733 odict = self.__dict__ # get attribute dictionary
768 734 return odict
769 735
770 736 def __setstate__(self, dict):
771 737 self.__dict__ = dict
772 738
773 739 __setattr__ = dict.__setitem__
774 740 __delattr__ = dict.__delitem__
775 741
776 742
777 743 class StrictAttributeDict(AttributeDictBase):
778 744 """
779 745 Strict Version of Attribute dict which raises an Attribute error when
780 746 requested attribute is not set
781 747 """
782 748 def __getattr__(self, attr):
783 749 try:
784 750 return self[attr]
785 751 except KeyError:
786 752 raise AttributeError('%s object has no attribute %s' % (
787 753 self.__class__, attr))
788 754
789 755
790 756 class AttributeDict(AttributeDictBase):
791 757 def __getattr__(self, attr):
792 758 return self.get(attr, None)
793 759
794 760
795 761
796 762 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
797 763 def __init__(self, default_factory=None, *args, **kwargs):
798 764 # in python3 you can omit the args to super
799 765 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
800 766 self.default_factory = default_factory
801 767
802 768
803 769 def fix_PATH(os_=None):
804 770 """
805 771 Get current active python path, and append it to PATH variable to fix
806 772 issues of subprocess calls and different python versions
807 773 """
808 774 if os_ is None:
809 775 import os
810 776 else:
811 777 os = os_
812 778
813 779 cur_path = os.path.split(sys.executable)[0]
814 780 if not os.environ['PATH'].startswith(cur_path):
815 781 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
816 782
817 783
818 784 def obfuscate_url_pw(engine):
819 785 _url = engine or ''
820 786 try:
821 787 _url = sqlalchemy.engine.url.make_url(engine)
822 788 if _url.password:
823 789 _url.password = 'XXXXX'
824 790 except Exception:
825 791 pass
826 792 return unicode(_url)
827 793
828 794
829 795 def get_server_url(environ):
830 796 req = webob.Request(environ)
831 797 return req.host_url + req.script_name
832 798
833 799
834 800 def unique_id(hexlen=32):
835 801 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
836 802 return suuid(truncate_to=hexlen, alphabet=alphabet)
837 803
838 804
839 805 def suuid(url=None, truncate_to=22, alphabet=None):
840 806 """
841 807 Generate and return a short URL safe UUID.
842 808
843 809 If the url parameter is provided, set the namespace to the provided
844 810 URL and generate a UUID.
845 811
846 812 :param url to get the uuid for
847 813 :truncate_to: truncate the basic 22 UUID to shorter version
848 814
849 815 The IDs won't be universally unique any longer, but the probability of
850 816 a collision will still be very low.
851 817 """
852 818 # Define our alphabet.
853 819 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
854 820
855 821 # If no URL is given, generate a random UUID.
856 822 if url is None:
857 823 unique_id = uuid.uuid4().int
858 824 else:
859 825 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
860 826
861 827 alphabet_length = len(_ALPHABET)
862 828 output = []
863 829 while unique_id > 0:
864 830 digit = unique_id % alphabet_length
865 831 output.append(_ALPHABET[digit])
866 832 unique_id = int(unique_id / alphabet_length)
867 833 return "".join(output)[:truncate_to]
868 834
869 835
870 836 def get_current_rhodecode_user(request=None):
871 837 """
872 838 Gets rhodecode user from request
873 839 """
874 840 pyramid_request = request or pyramid.threadlocal.get_current_request()
875 841
876 842 # web case
877 843 if pyramid_request and hasattr(pyramid_request, 'user'):
878 844 return pyramid_request.user
879 845
880 846 # api case
881 847 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
882 848 return pyramid_request.rpc_user
883 849
884 850 return None
885 851
886 852
887 853 def action_logger_generic(action, namespace=''):
888 854 """
889 855 A generic logger for actions useful to the system overview, tries to find
890 856 an acting user for the context of the call otherwise reports unknown user
891 857
892 858 :param action: logging message eg 'comment 5 deleted'
893 859 :param type: string
894 860
895 861 :param namespace: namespace of the logging message eg. 'repo.comments'
896 862 :param type: string
897 863
898 864 """
899 865
900 866 logger_name = 'rhodecode.actions'
901 867
902 868 if namespace:
903 869 logger_name += '.' + namespace
904 870
905 871 log = logging.getLogger(logger_name)
906 872
907 873 # get a user if we can
908 874 user = get_current_rhodecode_user()
909 875
910 876 logfunc = log.info
911 877
912 878 if not user:
913 879 user = '<unknown user>'
914 880 logfunc = log.warning
915 881
916 882 logfunc('Logging action by {}: {}'.format(user, action))
917 883
918 884
919 885 def escape_split(text, sep=',', maxsplit=-1):
920 886 r"""
921 887 Allows for escaping of the separator: e.g. arg='foo\, bar'
922 888
923 889 It should be noted that the way bash et. al. do command line parsing, those
924 890 single quotes are required.
925 891 """
926 892 escaped_sep = r'\%s' % sep
927 893
928 894 if escaped_sep not in text:
929 895 return text.split(sep, maxsplit)
930 896
931 897 before, _mid, after = text.partition(escaped_sep)
932 898 startlist = before.split(sep, maxsplit) # a regular split is fine here
933 899 unfinished = startlist[-1]
934 900 startlist = startlist[:-1]
935 901
936 902 # recurse because there may be more escaped separators
937 903 endlist = escape_split(after, sep, maxsplit)
938 904
939 905 # finish building the escaped value. we use endlist[0] becaue the first
940 906 # part of the string sent in recursion is the rest of the escaped value.
941 907 unfinished += sep + endlist[0]
942 908
943 909 return startlist + [unfinished] + endlist[1:] # put together all the parts
944 910
945 911
946 912 class OptionalAttr(object):
947 913 """
948 914 Special Optional Option that defines other attribute. Example::
949 915
950 916 def test(apiuser, userid=Optional(OAttr('apiuser')):
951 917 user = Optional.extract(userid)
952 918 # calls
953 919
954 920 """
955 921
956 922 def __init__(self, attr_name):
957 923 self.attr_name = attr_name
958 924
959 925 def __repr__(self):
960 926 return '<OptionalAttr:%s>' % self.attr_name
961 927
962 928 def __call__(self):
963 929 return self
964 930
965 931
966 932 # alias
967 933 OAttr = OptionalAttr
968 934
969 935
970 936 class Optional(object):
971 937 """
972 938 Defines an optional parameter::
973 939
974 940 param = param.getval() if isinstance(param, Optional) else param
975 941 param = param() if isinstance(param, Optional) else param
976 942
977 943 is equivalent of::
978 944
979 945 param = Optional.extract(param)
980 946
981 947 """
982 948
983 949 def __init__(self, type_):
984 950 self.type_ = type_
985 951
986 952 def __repr__(self):
987 953 return '<Optional:%s>' % self.type_.__repr__()
988 954
989 955 def __call__(self):
990 956 return self.getval()
991 957
992 958 def getval(self):
993 959 """
994 960 returns value from this Optional instance
995 961 """
996 962 if isinstance(self.type_, OAttr):
997 963 # use params name
998 964 return self.type_.attr_name
999 965 return self.type_
1000 966
1001 967 @classmethod
1002 968 def extract(cls, val):
1003 969 """
1004 970 Extracts value from Optional() instance
1005 971
1006 972 :param val:
1007 973 :return: original value if it's not Optional instance else
1008 974 value of instance
1009 975 """
1010 976 if isinstance(val, cls):
1011 977 return val.getval()
1012 978 return val
1013 979
1014 980
1015 981 def glob2re(pat):
1016 982 """
1017 983 Translate a shell PATTERN to a regular expression.
1018 984
1019 985 There is no way to quote meta-characters.
1020 986 """
1021 987
1022 988 i, n = 0, len(pat)
1023 989 res = ''
1024 990 while i < n:
1025 991 c = pat[i]
1026 992 i = i+1
1027 993 if c == '*':
1028 994 #res = res + '.*'
1029 995 res = res + '[^/]*'
1030 996 elif c == '?':
1031 997 #res = res + '.'
1032 998 res = res + '[^/]'
1033 999 elif c == '[':
1034 1000 j = i
1035 1001 if j < n and pat[j] == '!':
1036 1002 j = j+1
1037 1003 if j < n and pat[j] == ']':
1038 1004 j = j+1
1039 1005 while j < n and pat[j] != ']':
1040 1006 j = j+1
1041 1007 if j >= n:
1042 1008 res = res + '\\['
1043 1009 else:
1044 1010 stuff = pat[i:j].replace('\\','\\\\')
1045 1011 i = j+1
1046 1012 if stuff[0] == '!':
1047 1013 stuff = '^' + stuff[1:]
1048 1014 elif stuff[0] == '^':
1049 1015 stuff = '\\' + stuff
1050 1016 res = '%s[%s]' % (res, stuff)
1051 1017 else:
1052 1018 res = res + re.escape(c)
1053 1019 return res + '\Z(?ms)'
1054 1020
1055 1021
1056 1022 def parse_byte_string(size_str):
1057 1023 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1058 1024 if not match:
1059 1025 raise ValueError('Given size:%s is invalid, please make sure '
1060 1026 'to use format of <num>(MB|KB)' % size_str)
1061 1027
1062 1028 _parts = match.groups()
1063 1029 num, type_ = _parts
1064 1030 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1065 1031
1066 1032
1067 1033 class CachedProperty(object):
1068 1034 """
1069 1035 Lazy Attributes. With option to invalidate the cache by running a method
1070 1036
1071 1037 class Foo():
1072 1038
1073 1039 @CachedProperty
1074 1040 def heavy_func():
1075 1041 return 'super-calculation'
1076 1042
1077 1043 foo = Foo()
1078 1044 foo.heavy_func() # first computions
1079 1045 foo.heavy_func() # fetch from cache
1080 1046 foo._invalidate_prop_cache('heavy_func')
1081 1047 # at this point calling foo.heavy_func() will be re-computed
1082 1048 """
1083 1049
1084 1050 def __init__(self, func, func_name=None):
1085 1051
1086 1052 if func_name is None:
1087 1053 func_name = func.__name__
1088 1054 self.data = (func, func_name)
1089 1055 update_wrapper(self, func)
1090 1056
1091 1057 def __get__(self, inst, class_):
1092 1058 if inst is None:
1093 1059 return self
1094 1060
1095 1061 func, func_name = self.data
1096 1062 value = func(inst)
1097 1063 inst.__dict__[func_name] = value
1098 1064 if '_invalidate_prop_cache' not in inst.__dict__:
1099 1065 inst.__dict__['_invalidate_prop_cache'] = partial(
1100 1066 self._invalidate_prop_cache, inst)
1101 1067 return value
1102 1068
1103 1069 def _invalidate_prop_cache(self, inst, name):
1104 1070 inst.__dict__.pop(name, None)
@@ -1,746 +1,739 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Package for testing various lib/helper functions in rhodecode
24 24 """
25 25
26 26 import datetime
27 27 import string
28 28 import mock
29 29 import pytest
30 30
31 31 from rhodecode.tests import no_newline_id_generator
32 32 from rhodecode.tests.utils import run_test_concurrently
33 33
34 34 from rhodecode.lib import rc_cache
35 35 from rhodecode.lib.helpers import InitialsGravatar
36 36 from rhodecode.lib.utils2 import AttributeDict
37 37
38 38 from rhodecode.model.db import Repository, CacheKey
39 39
40 40
41 def _urls_for_proto(proto):
42 return [
43 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
44 '%s://127.0.0.1' % proto),
45 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
46 '%s://127.0.0.1' % proto),
47 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
48 '%s://127.0.0.1' % proto),
49 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
50 '%s://127.0.0.1:8080' % proto),
51 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
52 '%s://domain.org' % proto),
53 ('%s://user:pass@domain.org:8080' % proto,
54 ['%s://' % proto, 'domain.org', '8080'],
55 '%s://domain.org:8080' % proto),
41 TEST_URLS = [
42 ('127.0.0.1', '127.0.0.1'),
43 ('marcink@127.0.0.1', '127.0.0.1'),
44 ('marcink:pass@127.0.0.1', '127.0.0.1'),
45 ('marcink@domain.name:pass@127.0.0.1', '127.0.0.1'),
46
47 ('127.0.0.1:8080', '127.0.0.1:8080'),
48 ('marcink@127.0.0.1:8080', '127.0.0.1:8080'),
49 ('marcink:pass@127.0.0.1:8080', '127.0.0.1:8080'),
50 ('marcink@domain.name:pass@127.0.0.1:8080', '127.0.0.1:8080'),
51
52 ('domain.org', 'domain.org'),
53 ('user:pass@domain.org:8080', 'domain.org:8080'),
54 ('user@domain.org:pass@domain.org:8080', 'domain.org:8080'),
56 55 ]
57 56
58 TEST_URLS = _urls_for_proto('http') + _urls_for_proto('https')
59 57
60
61 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
62 def test_uri_filter(test_url, expected, expected_creds):
63 from rhodecode.lib.utils2 import uri_filter
64 assert uri_filter(test_url) == expected
65
66
67 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
68 def test_credentials_filter(test_url, expected, expected_creds):
58 @pytest.mark.parametrize("protocol", ['http://', 'https://'])
59 @pytest.mark.parametrize("test_url, expected", TEST_URLS)
60 def test_credentials_filter(protocol, test_url, expected):
69 61 from rhodecode.lib.utils2 import credentials_filter
70 assert credentials_filter(test_url) == expected_creds
62 test_url = protocol + test_url
63 assert credentials_filter(test_url) == protocol + expected
71 64
72 65
73 66 @pytest.mark.parametrize("str_bool, expected", [
74 67 ('t', True),
75 68 ('true', True),
76 69 ('y', True),
77 70 ('yes', True),
78 71 ('on', True),
79 72 ('1', True),
80 73 ('Y', True),
81 74 ('yeS', True),
82 75 ('Y', True),
83 76 ('TRUE', True),
84 77 ('T', True),
85 78 ('False', False),
86 79 ('F', False),
87 80 ('FALSE', False),
88 81 ('0', False),
89 82 ('-1', False),
90 83 ('', False)
91 84 ])
92 85 def test_str2bool(str_bool, expected):
93 86 from rhodecode.lib.utils2 import str2bool
94 87 assert str2bool(str_bool) == expected
95 88
96 89
97 90 @pytest.mark.parametrize("text, expected", reduce(lambda a1,a2:a1+a2, [
98 91 [
99 92 (pref+"", []),
100 93 (pref+"Hi there @marcink", ['marcink']),
101 94 (pref+"Hi there @marcink and @bob", ['bob', 'marcink']),
102 95 (pref+"Hi there @marcink\n", ['marcink']),
103 96 (pref+"Hi there @marcink and @bob\n", ['bob', 'marcink']),
104 97 (pref+"Hi there marcin@rhodecode.com", []),
105 98 (pref+"Hi there @john.malcovic and @bob\n", ['bob', 'john.malcovic']),
106 99 (pref+"This needs to be reviewed: (@marcink,@john)", ["john", "marcink"]),
107 100 (pref+"This needs to be reviewed: (@marcink, @john)", ["john", "marcink"]),
108 101 (pref+"This needs to be reviewed: [@marcink,@john]", ["john", "marcink"]),
109 102 (pref+"This needs to be reviewed: (@marcink @john)", ["john", "marcink"]),
110 103 (pref+"@john @mary, please review", ["john", "mary"]),
111 104 (pref+"@john,@mary, please review", ["john", "mary"]),
112 105 (pref+"Hej @123, @22john,@mary, please review", ['123', '22john', 'mary']),
113 106 (pref+"@first hi there @marcink here's my email marcin@email.com "
114 107 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three ", ['first', 'lukaszb', 'marcink', 'one', 'one_more22']),
115 108 (pref+"@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl", ['2one_more22', 'john', 'MARCIN', 'maRCiN']),
116 109 (pref+"@marian.user just do it @marco-polo and next extract @marco_polo", ['marco-polo', 'marco_polo', 'marian.user']),
117 110 (pref+"user.dot hej ! not-needed maril@domain.org", []),
118 111 (pref+"\n@marcin", ['marcin']),
119 112 ]
120 113 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
121 114 def test_mention_extractor(text, expected):
122 115 from rhodecode.lib.utils2 import extract_mentioned_users
123 116 got = extract_mentioned_users(text)
124 117 assert sorted(got, key=lambda x: x.lower()) == got
125 118 assert set(expected) == set(got)
126 119
127 120 @pytest.mark.parametrize("age_args, expected, kw", [
128 121 ({}, u'just now', {}),
129 122 ({'seconds': -1}, u'1 second ago', {}),
130 123 ({'seconds': -60 * 2}, u'2 minutes ago', {}),
131 124 ({'hours': -1}, u'1 hour ago', {}),
132 125 ({'hours': -24}, u'1 day ago', {}),
133 126 ({'hours': -24 * 5}, u'5 days ago', {}),
134 127 ({'months': -1}, u'1 month ago', {}),
135 128 ({'months': -1, 'days': -2}, u'1 month and 2 days ago', {}),
136 129 ({'years': -1, 'months': -1}, u'1 year and 1 month ago', {}),
137 130 ({}, u'just now', {'short_format': True}),
138 131 ({'seconds': -1}, u'1sec ago', {'short_format': True}),
139 132 ({'seconds': -60 * 2}, u'2min ago', {'short_format': True}),
140 133 ({'hours': -1}, u'1h ago', {'short_format': True}),
141 134 ({'hours': -24}, u'1d ago', {'short_format': True}),
142 135 ({'hours': -24 * 5}, u'5d ago', {'short_format': True}),
143 136 ({'months': -1}, u'1m ago', {'short_format': True}),
144 137 ({'months': -1, 'days': -2}, u'1m, 2d ago', {'short_format': True}),
145 138 ({'years': -1, 'months': -1}, u'1y, 1m ago', {'short_format': True}),
146 139 ])
147 140 def test_age(age_args, expected, kw, baseapp):
148 141 from rhodecode.lib.utils2 import age
149 142 from dateutil import relativedelta
150 143 n = datetime.datetime(year=2012, month=5, day=17)
151 144 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
152 145
153 146 def translate(elem):
154 147 return elem.interpolate()
155 148
156 149 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
157 150
158 151
159 152 @pytest.mark.parametrize("age_args, expected, kw", [
160 153 ({}, u'just now', {}),
161 154 ({'seconds': 1}, u'in 1 second', {}),
162 155 ({'seconds': 60 * 2}, u'in 2 minutes', {}),
163 156 ({'hours': 1}, u'in 1 hour', {}),
164 157 ({'hours': 24}, u'in 1 day', {}),
165 158 ({'hours': 24 * 5}, u'in 5 days', {}),
166 159 ({'months': 1}, u'in 1 month', {}),
167 160 ({'months': 1, 'days': 1}, u'in 1 month and 1 day', {}),
168 161 ({'years': 1, 'months': 1}, u'in 1 year and 1 month', {}),
169 162 ({}, u'just now', {'short_format': True}),
170 163 ({'seconds': 1}, u'in 1sec', {'short_format': True}),
171 164 ({'seconds': 60 * 2}, u'in 2min', {'short_format': True}),
172 165 ({'hours': 1}, u'in 1h', {'short_format': True}),
173 166 ({'hours': 24}, u'in 1d', {'short_format': True}),
174 167 ({'hours': 24 * 5}, u'in 5d', {'short_format': True}),
175 168 ({'months': 1}, u'in 1m', {'short_format': True}),
176 169 ({'months': 1, 'days': 1}, u'in 1m, 1d', {'short_format': True}),
177 170 ({'years': 1, 'months': 1}, u'in 1y, 1m', {'short_format': True}),
178 171 ])
179 172 def test_age_in_future(age_args, expected, kw, baseapp):
180 173 from rhodecode.lib.utils2 import age
181 174 from dateutil import relativedelta
182 175 n = datetime.datetime(year=2012, month=5, day=17)
183 176 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
184 177
185 178 def translate(elem):
186 179 return elem.interpolate()
187 180
188 181 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
189 182
190 183
191 184 @pytest.mark.parametrize("sample, expected_tags", [
192 185 # entry
193 186 ((
194 187 ""
195 188 ),
196 189 [
197 190
198 191 ]),
199 192 # entry
200 193 ((
201 194 "hello world [stale]"
202 195 ),
203 196 [
204 197 ('state', '[stale]'),
205 198 ]),
206 199 # entry
207 200 ((
208 201 "hello world [v2.0.0] [v1.0.0]"
209 202 ),
210 203 [
211 204 ('generic', '[v2.0.0]'),
212 205 ('generic', '[v1.0.0]'),
213 206 ]),
214 207 # entry
215 208 ((
216 209 "he[ll]o wo[rl]d"
217 210 ),
218 211 [
219 212 ('label', '[ll]'),
220 213 ('label', '[rl]'),
221 214 ]),
222 215 # entry
223 216 ((
224 217 "hello world [stale]\n[featured]\n[stale] [dead] [dev]"
225 218 ),
226 219 [
227 220 ('state', '[stale]'),
228 221 ('state', '[featured]'),
229 222 ('state', '[stale]'),
230 223 ('state', '[dead]'),
231 224 ('state', '[dev]'),
232 225 ]),
233 226 # entry
234 227 ((
235 228 "hello world \n\n [stale] \n [url =&gt; [name](http://rc.com)]"
236 229 ),
237 230 [
238 231 ('state', '[stale]'),
239 232 ('url', '[url =&gt; [name](http://rc.com)]'),
240 233 ]),
241 234 # entry
242 235 ((
243 236 "[url =&gt; [linkNameJS](javascript:alert(document.domain))]\n"
244 237 "[url =&gt; [linkNameHTTP](http://rhodecode.com)]\n"
245 238 "[url =&gt; [linkNameHTTPS](https://rhodecode.com)]\n"
246 239 "[url =&gt; [linkNamePath](/repo_group)]\n"
247 240 ),
248 241 [
249 242 ('generic', '[linkNameJS]'),
250 243 ('url', '[url =&gt; [linkNameHTTP](http://rhodecode.com)]'),
251 244 ('url', '[url =&gt; [linkNameHTTPS](https://rhodecode.com)]'),
252 245 ('url', '[url =&gt; [linkNamePath](/repo_group)]'),
253 246 ]),
254 247 # entry
255 248 ((
256 249 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =&gt;>< sa]"
257 250 "[requires] [stale] [see<>=&gt;] [see =&gt; http://url.com]"
258 251 "[requires =&gt; url] [lang =&gt; python] [just a tag] "
259 252 "<html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
260 253 "[,d] [ =&gt; ULR ] [obsolete] [desc]]"
261 254 ),
262 255 [
263 256 ('label', '[desc]'),
264 257 ('label', '[obsolete]'),
265 258 ('label', '[or]'),
266 259 ('label', '[requires]'),
267 260 ('label', '[tag]'),
268 261 ('state', '[stale]'),
269 262 ('lang', '[lang =&gt; python]'),
270 263 ('ref', '[requires =&gt; url]'),
271 264 ('see', '[see =&gt; http://url.com]'),
272 265
273 266 ]),
274 267
275 268 ], ids=no_newline_id_generator)
276 269 def test_metatag_extraction(sample, expected_tags):
277 270 from rhodecode.lib.helpers import extract_metatags
278 271 tags, value = extract_metatags(sample)
279 272 assert sorted(tags) == sorted(expected_tags)
280 273
281 274
282 275 @pytest.mark.parametrize("tag_data, expected_html", [
283 276
284 277 (('state', '[stable]'), '<div class="metatag" tag="state stable">stable</div>'),
285 278 (('state', '[stale]'), '<div class="metatag" tag="state stale">stale</div>'),
286 279 (('state', '[featured]'), '<div class="metatag" tag="state featured">featured</div>'),
287 280 (('state', '[dev]'), '<div class="metatag" tag="state dev">dev</div>'),
288 281 (('state', '[dead]'), '<div class="metatag" tag="state dead">dead</div>'),
289 282
290 283 (('label', '[personal]'), '<div class="metatag" tag="label">personal</div>'),
291 284 (('generic', '[v2.0.0]'), '<div class="metatag" tag="generic">v2.0.0</div>'),
292 285
293 286 (('lang', '[lang =&gt; JavaScript]'), '<div class="metatag" tag="lang">JavaScript</div>'),
294 287 (('lang', '[lang =&gt; C++]'), '<div class="metatag" tag="lang">C++</div>'),
295 288 (('lang', '[lang =&gt; C#]'), '<div class="metatag" tag="lang">C#</div>'),
296 289 (('lang', '[lang =&gt; Delphi/Object]'), '<div class="metatag" tag="lang">Delphi/Object</div>'),
297 290 (('lang', '[lang =&gt; Objective-C]'), '<div class="metatag" tag="lang">Objective-C</div>'),
298 291 (('lang', '[lang =&gt; .NET]'), '<div class="metatag" tag="lang">.NET</div>'),
299 292
300 293 (('license', '[license =&gt; BSD 3-clause]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>'),
301 294 (('license', '[license =&gt; GPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>'),
302 295 (('license', '[license =&gt; MIT]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>'),
303 296 (('license', '[license =&gt; AGPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>'),
304 297
305 298 (('ref', '[requires =&gt; RepoName]'), '<div class="metatag" tag="ref requires">requires: <a href="/RepoName">RepoName</a></div>'),
306 299 (('ref', '[recommends =&gt; GroupName]'), '<div class="metatag" tag="ref recommends">recommends: <a href="/GroupName">GroupName</a></div>'),
307 300 (('ref', '[conflicts =&gt; SomeName]'), '<div class="metatag" tag="ref conflicts">conflicts: <a href="/SomeName">SomeName</a></div>'),
308 301 (('ref', '[base =&gt; SomeName]'), '<div class="metatag" tag="ref base">base: <a href="/SomeName">SomeName</a></div>'),
309 302
310 303 (('see', '[see =&gt; http://rhodecode.com]'), '<div class="metatag" tag="see">see: http://rhodecode.com </div>'),
311 304
312 305 (('url', '[url =&gt; [linkName](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">linkName</a> </div>'),
313 306 (('url', '[url =&gt; [example link](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">example link</a> </div>'),
314 307 (('url', '[url =&gt; [v1.0.0](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">v1.0.0</a> </div>'),
315 308
316 309 ])
317 310 def test_metatags_stylize(tag_data, expected_html):
318 311 from rhodecode.lib.helpers import style_metatag
319 312 tag_type,value = tag_data
320 313 assert style_metatag(tag_type, value) == expected_html
321 314
322 315
323 316 @pytest.mark.parametrize("tmpl_url, email, expected", [
324 317 ('http://test.com/{email}', 'test@foo.com', 'http://test.com/test@foo.com'),
325 318
326 319 ('http://test.com/{md5email}', 'test@foo.com', 'http://test.com/3cb7232fcc48743000cb86d0d5022bd9'),
327 320 ('http://test.com/{md5email}', 'testΔ…Δ‡@foo.com', 'http://test.com/978debb907a3c55cd741872ab293ef30'),
328 321
329 322 ('http://testX.com/{md5email}?s={size}', 'test@foo.com', 'http://testX.com/3cb7232fcc48743000cb86d0d5022bd9?s=24'),
330 323 ('http://testX.com/{md5email}?s={size}', 'testΔ…Δ‡@foo.com', 'http://testX.com/978debb907a3c55cd741872ab293ef30?s=24'),
331 324
332 325 ('{scheme}://{netloc}/{md5email}/{size}', 'test@foo.com', 'https://server.com/3cb7232fcc48743000cb86d0d5022bd9/24'),
333 326 ('{scheme}://{netloc}/{md5email}/{size}', 'testΔ…Δ‡@foo.com', 'https://server.com/978debb907a3c55cd741872ab293ef30/24'),
334 327
335 328 ('http://test.com/{email}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com'),
336 329 ('http://test.com/{email}?size={size}', 'test@foo.com', 'http://test.com/test@foo.com?size=24'),
337 330 ('http://test.com/{email}?size={size}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com?size=24'),
338 331 ])
339 332 def test_gravatar_url_builder(tmpl_url, email, expected, request_stub):
340 333 from rhodecode.lib.helpers import gravatar_url
341 334
342 335 def fake_tmpl_context(_url):
343 336 _c = AttributeDict()
344 337 _c.visual = AttributeDict()
345 338 _c.visual.use_gravatar = True
346 339 _c.visual.gravatar_url = _url
347 340 return _c
348 341
349 342 # mock pyramid.threadlocals
350 343 def fake_get_current_request():
351 344 request_stub.scheme = 'https'
352 345 request_stub.host = 'server.com'
353 346
354 347 request_stub._call_context = fake_tmpl_context(tmpl_url)
355 348 return request_stub
356 349
357 350 with mock.patch('rhodecode.lib.helpers.get_current_request',
358 351 fake_get_current_request):
359 352
360 353 grav = gravatar_url(email_address=email, size=24)
361 354 assert grav == expected
362 355
363 356
364 357 @pytest.mark.parametrize(
365 358 "email, first_name, last_name, expected_initials, expected_color", [
366 359
367 360 ('test@rhodecode.com', '', '', 'TR', '#8a994d'),
368 361 ('marcin.kuzminski@rhodecode.com', '', '', 'MK', '#6559b3'),
369 362 # special cases of email
370 363 ('john.van.dam@rhodecode.com', '', '', 'JD', '#526600'),
371 364 ('Guido.van.Rossum@rhodecode.com', '', '', 'GR', '#990052'),
372 365 ('Guido.van.Rossum@rhodecode.com', 'Guido', 'Van Rossum', 'GR', '#990052'),
373 366
374 367 ('rhodecode+Guido.van.Rossum@rhodecode.com', '', '', 'RR', '#46598c'),
375 368 ('pclouds@rhodecode.com', 'Nguyα»…n ThΓ‘i', 'Tgọc Duy', 'ND', '#665200'),
376 369
377 370 ('john-brown@foo.com', '', '', 'JF', '#73006b'),
378 371 ('admin@rhodecode.com', 'Marcin', 'Kuzminski', 'MK', '#104036'),
379 372 # partials
380 373 ('admin@rhodecode.com', 'Marcin', '', 'MR', '#104036'), # fn+email
381 374 ('admin@rhodecode.com', '', 'Kuzminski', 'AK', '#104036'), # em+ln
382 375 # non-ascii
383 376 ('admin@rhodecode.com', 'Marcin', 'Śuzminski', 'MS', '#104036'),
384 377 ('marcin.Ε›uzminski@rhodecode.com', '', '', 'MS', '#73000f'),
385 378
386 379 # special cases, LDAP can provide those...
387 380 ('admin@', 'Marcin', 'Śuzminski', 'MS', '#aa00ff'),
388 381 ('marcin.Ε›uzminski', '', '', 'MS', '#402020'),
389 382 ('null', '', '', 'NL', '#8c4646'),
390 383 ('some.@abc.com', 'some', '', 'SA', '#664e33')
391 384 ])
392 385 def test_initials_gravatar_pick_of_initials_and_color_algo(
393 386 email, first_name, last_name, expected_initials, expected_color):
394 387 instance = InitialsGravatar(email, first_name, last_name)
395 388 assert instance.get_initials() == expected_initials
396 389 assert instance.str2color(email) == expected_color
397 390
398 391
399 392 def test_initials_gravatar_mapping_algo():
400 393 pos = set()
401 394 instance = InitialsGravatar('', '', '')
402 395 iterations = 0
403 396
404 397 variations = []
405 398 for letter1 in string.ascii_letters:
406 399 for letter2 in string.ascii_letters[::-1][:10]:
407 400 for letter3 in string.ascii_letters[:10]:
408 401 variations.append(
409 402 '%s@rhodecode.com' % (letter1+letter2+letter3))
410 403
411 404 max_variations = 4096
412 405 for email in variations[:max_variations]:
413 406 iterations += 1
414 407 pos.add(
415 408 instance.pick_color_bank_index(email,
416 409 instance.get_color_bank()))
417 410
418 411 # we assume that we have match all 256 possible positions,
419 412 # in reasonable amount of different email addresses
420 413 assert len(pos) == 256
421 414 assert iterations == max_variations
422 415
423 416
424 417 @pytest.mark.parametrize("tmpl, repo_name, overrides, prefix, expected", [
425 418 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '', 'http://vps1:8000/group/repo1'),
426 419 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/group/repo1'),
427 420 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '/rc', 'http://vps1:8000/rc/group/repo1'),
428 421 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc', 'http://user@vps1:8000/rc/group/repo1'),
429 422 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc', 'http://marcink@vps1:8000/rc/group/repo1'),
430 423 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc/', 'http://user@vps1:8000/rc/group/repo1'),
431 424 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc/', 'http://marcink@vps1:8000/rc/group/repo1'),
432 425 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {}, '', 'http://vps1:8000/_23'),
433 426 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
434 427 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
435 428 ('http://{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://vps1:8000/_23'),
436 429 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://marcink@proxy1.server.com/group/repo1'),
437 430 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {}, '', 'https://proxy1.server.com/group/repo1'),
438 431 ('https://proxy1.server.com/{user}/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://proxy1.server.com/marcink/group/repo1'),
439 432 ])
440 433 def test_clone_url_generator(tmpl, repo_name, overrides, prefix, expected):
441 434 from rhodecode.lib.utils2 import get_clone_url
442 435
443 436 class RequestStub(object):
444 437 def request_url(self, name):
445 438 return 'http://vps1:8000' + prefix
446 439
447 440 def route_url(self, name):
448 441 return self.request_url(name)
449 442
450 443 clone_url = get_clone_url(
451 444 request=RequestStub(),
452 445 uri_tmpl=tmpl,
453 446 repo_name=repo_name, repo_id=23, repo_type='hg', **overrides)
454 447 assert clone_url == expected
455 448
456 449
457 450 def test_clone_url_svn_ssh_generator():
458 451 from rhodecode.lib.utils2 import get_clone_url
459 452
460 453 class RequestStub(object):
461 454 def request_url(self, name):
462 455 return 'http://vps1:8000'
463 456
464 457 def route_url(self, name):
465 458 return self.request_url(name)
466 459
467 460 clone_url = get_clone_url(
468 461 request=RequestStub(),
469 462 uri_tmpl=Repository.DEFAULT_CLONE_URI_SSH,
470 463 repo_name='svn-test', repo_id=23, repo_type='svn', **{'sys_user': 'rcdev'})
471 464 assert clone_url == 'svn+ssh://rcdev@vps1/svn-test'
472 465
473 466
474 467 idx = 0
475 468
476 469
477 470 def _quick_url(text, tmpl="""<a class="tooltip-hovercard revision-link" href="%s" data-hovercard-alt="Commit: %s" data-hovercard-url="/some-url">%s</a>""", url_=None, commits=''):
478 471 """
479 472 Changes `some text url[foo]` => `some text <a href="/">foo</a>
480 473
481 474 :param text:
482 475 """
483 476 import re
484 477 # quickly change expected url[] into a link
485 478 url_pat = re.compile(r'(?:url\[)(.+?)(?:\])')
486 479 commits = commits or []
487 480
488 481 global idx
489 482 idx = 0
490 483
491 484 def url_func(match_obj):
492 485 global idx
493 486 _url = match_obj.groups()[0]
494 487 if commits:
495 488 commit = commits[idx]
496 489 idx += 1
497 490 return tmpl % (url_ or '/some-url', _url, commit)
498 491 else:
499 492 return tmpl % (url_ or '/some-url', _url)
500 493
501 494 return url_pat.sub(url_func, text)
502 495
503 496
504 497 @pytest.mark.parametrize("sample, expected, commits", [
505 498 (
506 499 "",
507 500 "",
508 501 [""]
509 502 ),
510 503 (
511 504 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
512 505 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
513 506 [""]
514 507 ),
515 508 (
516 509 "from rev 000000000000",
517 510 "from rev url[000000000000]",
518 511 ["000000000000"]
519 512 ),
520 513
521 514 (
522 515 "from rev 000000000000123123 also rev 000000000000",
523 516 "from rev url[000000000000123123] also rev url[000000000000]",
524 517 ["000000000000123123", "000000000000"]
525 518 ),
526 519 (
527 520 "this should-000 00",
528 521 "this should-000 00",
529 522 [""]
530 523 ),
531 524 (
532 525 "longtextffffffffff rev 123123123123",
533 526 "longtextffffffffff rev url[123123123123]",
534 527 ["123123123123"]
535 528 ),
536 529 (
537 530 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
538 531 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
539 532 ["ffffffffffffffffffffffffffffffffffffffffffffffffff"]
540 533 ),
541 534 (
542 535 "ffffffffffff some text traalaa",
543 536 "url[ffffffffffff] some text traalaa",
544 537 ["ffffffffffff"]
545 538 ),
546 539 (
547 540 """Multi line
548 541 123123123123
549 542 some text 000000000000
550 543 sometimes !
551 544 """,
552 545 """Multi line
553 546 url[123123123123]
554 547 some text url[000000000000]
555 548 sometimes !
556 549 """,
557 550 ["123123123123", "000000000000"]
558 551 )
559 552 ], ids=no_newline_id_generator)
560 553 def test_urlify_commits(sample, expected, commits):
561 554 def fake_url(self, *args, **kwargs):
562 555 return '/some-url'
563 556
564 557 expected = _quick_url(expected, commits=commits)
565 558
566 559 with mock.patch('rhodecode.lib.helpers.route_url', fake_url):
567 560 from rhodecode.lib.helpers import urlify_commits
568 561 assert urlify_commits(sample, 'repo_name') == expected
569 562
570 563
571 564 @pytest.mark.parametrize("sample, expected, url_", [
572 565 ("",
573 566 "",
574 567 ""),
575 568 ("https://svn.apache.org/repos",
576 569 "url[https://svn.apache.org/repos]",
577 570 "https://svn.apache.org/repos"),
578 571 ("http://svn.apache.org/repos",
579 572 "url[http://svn.apache.org/repos]",
580 573 "http://svn.apache.org/repos"),
581 574 ("from rev a also rev http://google.com",
582 575 "from rev a also rev url[http://google.com]",
583 576 "http://google.com"),
584 577 ("""Multi line
585 578 https://foo.bar.com
586 579 some text lalala""",
587 580 """Multi line
588 581 url[https://foo.bar.com]
589 582 some text lalala""",
590 583 "https://foo.bar.com")
591 584 ], ids=no_newline_id_generator)
592 585 def test_urlify_test(sample, expected, url_):
593 586 from rhodecode.lib.helpers import urlify_text
594 587 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
595 588 assert urlify_text(sample) == expected
596 589
597 590
598 591 @pytest.mark.parametrize("test, expected", [
599 592 ("", None),
600 593 ("/_2", '2'),
601 594 ("_2", '2'),
602 595 ("/_2/", '2'),
603 596 ("_2/", '2'),
604 597
605 598 ("/_21", '21'),
606 599 ("_21", '21'),
607 600 ("/_21/", '21'),
608 601 ("_21/", '21'),
609 602
610 603 ("/_21/foobar", '21'),
611 604 ("_21/121", '21'),
612 605 ("/_21/_12", '21'),
613 606 ("_21/rc/foo", '21'),
614 607
615 608 ])
616 609 def test_get_repo_by_id(test, expected):
617 610 from rhodecode.model.repo import RepoModel
618 611 _test = RepoModel()._extract_id_from_repo_name(test)
619 612 assert _test == expected
620 613
621 614
622 615 def test_invalidation_context(baseapp):
623 616 repo_id = 9999
624 617
625 618 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
626 619 repo_id, CacheKey.CACHE_TYPE_FEED)
627 620 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
628 621 repo_id=repo_id)
629 622 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
630 623
631 624 calls = [1, 2]
632 625
633 626 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
634 627 def _dummy_func(cache_key):
635 628 val = calls.pop(0)
636 629 return 'result:{}'.format(val)
637 630
638 631 inv_context_manager = rc_cache.InvalidationContext(
639 632 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
640 633
641 634 # 1st call, fresh caches
642 635 with inv_context_manager as invalidation_context:
643 636 should_invalidate = invalidation_context.should_invalidate()
644 637 if should_invalidate:
645 638 result = _dummy_func.refresh('some-key')
646 639 else:
647 640 result = _dummy_func('some-key')
648 641
649 642 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
650 643 assert should_invalidate is True
651 644
652 645 assert 'result:1' == result
653 646 # should be cached so calling it twice will give the same result !
654 647 result = _dummy_func('some-key')
655 648 assert 'result:1' == result
656 649
657 650 # 2nd call, we create a new context manager, this should be now key aware, and
658 651 # return an active cache region
659 652 with inv_context_manager as invalidation_context:
660 653 should_invalidate = invalidation_context.should_invalidate()
661 654 assert isinstance(invalidation_context, rc_cache.ActiveRegionCache)
662 655 assert should_invalidate is False
663 656
664 657 # Mark invalidation
665 658 CacheKey.set_invalidate(invalidation_namespace)
666 659
667 660 # 3nd call, fresh caches
668 661 with inv_context_manager as invalidation_context:
669 662 should_invalidate = invalidation_context.should_invalidate()
670 663 if should_invalidate:
671 664 result = _dummy_func.refresh('some-key')
672 665 else:
673 666 result = _dummy_func('some-key')
674 667
675 668 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
676 669 assert should_invalidate is True
677 670
678 671 assert 'result:2' == result
679 672
680 673 # cached again, same result
681 674 result = _dummy_func('some-key')
682 675 assert 'result:2' == result
683 676
684 677
685 678 def test_invalidation_context_exception_in_compute(baseapp):
686 679 repo_id = 888
687 680
688 681 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
689 682 repo_id, CacheKey.CACHE_TYPE_FEED)
690 683 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
691 684 repo_id=repo_id)
692 685 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
693 686
694 687 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
695 688 def _dummy_func(cache_key):
696 689 raise Exception('Error in cache func')
697 690
698 691 with pytest.raises(Exception):
699 692 inv_context_manager = rc_cache.InvalidationContext(
700 693 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
701 694
702 695 # 1st call, fresh caches
703 696 with inv_context_manager as invalidation_context:
704 697 should_invalidate = invalidation_context.should_invalidate()
705 698 if should_invalidate:
706 699 _dummy_func.refresh('some-key-2')
707 700 else:
708 701 _dummy_func('some-key-2')
709 702
710 703
711 704 @pytest.mark.parametrize('execution_number', range(5))
712 705 def test_cache_invalidation_race_condition(execution_number, baseapp):
713 706 import time
714 707
715 708 repo_id = 777
716 709
717 710 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
718 711 repo_id, CacheKey.CACHE_TYPE_FEED)
719 712 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
720 713 repo_id=repo_id)
721 714 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
722 715
723 716 @run_test_concurrently(25)
724 717 def test_create_and_delete_cache_keys():
725 718 time.sleep(0.2)
726 719
727 720 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
728 721 def _dummy_func(cache_key):
729 722 val = 'async'
730 723 return 'result:{}'.format(val)
731 724
732 725 inv_context_manager = rc_cache.InvalidationContext(
733 726 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
734 727
735 728 # 1st call, fresh caches
736 729 with inv_context_manager as invalidation_context:
737 730 should_invalidate = invalidation_context.should_invalidate()
738 731 if should_invalidate:
739 732 _dummy_func.refresh('some-key-3')
740 733 else:
741 734 _dummy_func('some-key-3')
742 735
743 736 # Mark invalidation
744 737 CacheKey.set_invalidate(invalidation_namespace)
745 738
746 739 test_create_and_delete_cache_keys()
General Comments 0
You need to be logged in to leave comments. Login now