##// END OF EJS Templates
svn: fix checkout SVN ssh url....
marcink -
r4133:23b8627d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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