Some of the non canononical timezones are not present in Ubuntu Noble, it would be a better practice to only use canonical timezones in data and tests. Note that this is not a real fix for all cases since the database that ran on Ubuntu Jammy and are moved to an ubuntu Noble server will have the issue with timezones already in database. One of the possible fix would be to manage that during upgrades, but this isn't a verry flexible solution since upgrade are meant to manage chyange of version, not change of server. If an old 17.0 versions needs to be moved to a Noble server, this won't work. Another solution would be to install package like tzdata-legacy that may keep the old timezones but it is not the only think since TAI-10 are also in this package. This solution is not ideal because non canonical timezone will still be shown in the dropdown. We would need to filter them. A last solution would be to add the support for those old timezones by monkeypatching the lib. This way, only new timezones would be shown but non canonical one won't crash when used. This is not ideal either because we may need to keep this for a while. But in combination with the upgrade solution, it may work proprely. Part-of: odoo/odoo#160842
631 lines
27 KiB
Python
631 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
import os.path
|
|
import pytz
|
|
|
|
from odoo.tools import (
|
|
config,
|
|
date_utils,
|
|
file_open,
|
|
file_path,
|
|
merge_sequences,
|
|
misc,
|
|
remove_accents,
|
|
validate_url,
|
|
)
|
|
from odoo.tests.common import TransactionCase, BaseCase
|
|
|
|
|
|
class TestCountingStream(BaseCase):
|
|
def test_empty_stream(self):
|
|
s = misc.CountingStream(iter([]))
|
|
self.assertEqual(s.index, -1)
|
|
self.assertIsNone(next(s, None))
|
|
self.assertEqual(s.index, 0)
|
|
|
|
def test_single(self):
|
|
s = misc.CountingStream(range(1))
|
|
self.assertEqual(s.index, -1)
|
|
self.assertEqual(next(s, None), 0)
|
|
self.assertIsNone(next(s, None))
|
|
self.assertEqual(s.index, 1)
|
|
|
|
def test_full(self):
|
|
s = misc.CountingStream(range(42))
|
|
for _ in s:
|
|
pass
|
|
self.assertEqual(s.index, 42)
|
|
|
|
def test_repeated(self):
|
|
""" Once the CountingStream has stopped iterating, the index should not
|
|
increase anymore (the internal state should not be allowed to change)
|
|
"""
|
|
s = misc.CountingStream(iter([]))
|
|
self.assertIsNone(next(s, None))
|
|
self.assertEqual(s.index, 0)
|
|
self.assertIsNone(next(s, None))
|
|
self.assertEqual(s.index, 0)
|
|
|
|
|
|
class TestMergeSequences(BaseCase):
|
|
def test_merge_sequences(self):
|
|
# base case
|
|
seq = merge_sequences(['A', 'B', 'C'])
|
|
self.assertEqual(seq, ['A', 'B', 'C'])
|
|
|
|
# 'Z' can be anywhere
|
|
seq = merge_sequences(['A', 'B', 'C'], ['Z'])
|
|
self.assertEqual(seq, ['A', 'B', 'C', 'Z'])
|
|
|
|
# 'Y' must precede 'C';
|
|
seq = merge_sequences(['A', 'B', 'C'], ['Y', 'C'])
|
|
self.assertEqual(seq, ['A', 'B', 'Y', 'C'])
|
|
|
|
# 'X' must follow 'A' and precede 'C'
|
|
seq = merge_sequences(['A', 'B', 'C'], ['A', 'X', 'C'])
|
|
self.assertEqual(seq, ['A', 'B', 'X', 'C'])
|
|
|
|
# all cases combined
|
|
seq = merge_sequences(
|
|
['A', 'B', 'C'],
|
|
['Z'], # 'Z' can be anywhere
|
|
['Y', 'C'], # 'Y' must precede 'C';
|
|
['A', 'X', 'Y'], # 'X' must follow 'A' and precede 'Y'
|
|
)
|
|
self.assertEqual(seq, ['A', 'B', 'X', 'Y', 'C', 'Z'])
|
|
|
|
|
|
class TestDateRangeFunction(BaseCase):
|
|
""" Test on date_range generator. """
|
|
|
|
def test_date_range_with_naive_datetimes(self):
|
|
""" Check date_range with naive datetimes. """
|
|
start = datetime.datetime(1985, 1, 1)
|
|
end = datetime.datetime(1986, 1, 1)
|
|
|
|
expected = [
|
|
datetime.datetime(1985, 1, 1, 0, 0),
|
|
datetime.datetime(1985, 2, 1, 0, 0),
|
|
datetime.datetime(1985, 3, 1, 0, 0),
|
|
datetime.datetime(1985, 4, 1, 0, 0),
|
|
datetime.datetime(1985, 5, 1, 0, 0),
|
|
datetime.datetime(1985, 6, 1, 0, 0),
|
|
datetime.datetime(1985, 7, 1, 0, 0),
|
|
datetime.datetime(1985, 8, 1, 0, 0),
|
|
datetime.datetime(1985, 9, 1, 0, 0),
|
|
datetime.datetime(1985, 10, 1, 0, 0),
|
|
datetime.datetime(1985, 11, 1, 0, 0),
|
|
datetime.datetime(1985, 12, 1, 0, 0),
|
|
datetime.datetime(1986, 1, 1, 0, 0)
|
|
]
|
|
|
|
dates = [date for date in date_utils.date_range(start, end)]
|
|
|
|
self.assertEqual(dates, expected)
|
|
|
|
def test_date_range_with_date(self):
|
|
""" Check date_range with naive datetimes. """
|
|
start = datetime.date(1985, 1, 1)
|
|
end = datetime.date(1986, 1, 1)
|
|
|
|
expected = [
|
|
datetime.date(1985, 1, 1),
|
|
datetime.date(1985, 2, 1),
|
|
datetime.date(1985, 3, 1),
|
|
datetime.date(1985, 4, 1),
|
|
datetime.date(1985, 5, 1),
|
|
datetime.date(1985, 6, 1),
|
|
datetime.date(1985, 7, 1),
|
|
datetime.date(1985, 8, 1),
|
|
datetime.date(1985, 9, 1),
|
|
datetime.date(1985, 10, 1),
|
|
datetime.date(1985, 11, 1),
|
|
datetime.date(1985, 12, 1),
|
|
datetime.date(1986, 1, 1),
|
|
]
|
|
|
|
self.assertEqual(list(date_utils.date_range(start, end)), expected)
|
|
|
|
def test_date_range_with_timezone_aware_datetimes_other_than_utc(self):
|
|
""" Check date_range with timezone-aware datetimes other than UTC."""
|
|
timezone = pytz.timezone('Europe/Brussels')
|
|
|
|
start = datetime.datetime(1985, 1, 1)
|
|
end = datetime.datetime(1986, 1, 1)
|
|
start = timezone.localize(start)
|
|
end = timezone.localize(end)
|
|
|
|
expected = [datetime.datetime(1985, 1, 1, 0, 0),
|
|
datetime.datetime(1985, 2, 1, 0, 0),
|
|
datetime.datetime(1985, 3, 1, 0, 0),
|
|
datetime.datetime(1985, 4, 1, 0, 0),
|
|
datetime.datetime(1985, 5, 1, 0, 0),
|
|
datetime.datetime(1985, 6, 1, 0, 0),
|
|
datetime.datetime(1985, 7, 1, 0, 0),
|
|
datetime.datetime(1985, 8, 1, 0, 0),
|
|
datetime.datetime(1985, 9, 1, 0, 0),
|
|
datetime.datetime(1985, 10, 1, 0, 0),
|
|
datetime.datetime(1985, 11, 1, 0, 0),
|
|
datetime.datetime(1985, 12, 1, 0, 0),
|
|
datetime.datetime(1986, 1, 1, 0, 0)]
|
|
|
|
expected = [timezone.localize(e) for e in expected]
|
|
|
|
dates = [date for date in date_utils.date_range(start, end)]
|
|
|
|
self.assertEqual(expected, dates)
|
|
|
|
def test_date_range_with_mismatching_zones(self):
|
|
""" Check date_range with mismatching zone should raise an exception."""
|
|
start_timezone = pytz.timezone('Europe/Brussels')
|
|
end_timezone = pytz.timezone('America/Recife')
|
|
|
|
start = datetime.datetime(1985, 1, 1)
|
|
end = datetime.datetime(1986, 1, 1)
|
|
start = start_timezone.localize(start)
|
|
end = end_timezone.localize(end)
|
|
|
|
with self.assertRaises(ValueError):
|
|
dates = [date for date in date_utils.date_range(start, end)]
|
|
|
|
def test_date_range_with_inconsistent_datetimes(self):
|
|
""" Check date_range with a timezone-aware datetime and a naive one."""
|
|
context_timezone = pytz.timezone('Europe/Brussels')
|
|
|
|
start = datetime.datetime(1985, 1, 1)
|
|
end = datetime.datetime(1986, 1, 1)
|
|
end = context_timezone.localize(end)
|
|
|
|
with self.assertRaises(ValueError):
|
|
dates = [date for date in date_utils.date_range(start, end)]
|
|
|
|
def test_date_range_with_hour(self):
|
|
""" Test date range with hour and naive datetime."""
|
|
start = datetime.datetime(2018, 3, 25)
|
|
end = datetime.datetime(2018, 3, 26)
|
|
step = relativedelta(hours=1)
|
|
|
|
expected = [
|
|
datetime.datetime(2018, 3, 25, 0, 0),
|
|
datetime.datetime(2018, 3, 25, 1, 0),
|
|
datetime.datetime(2018, 3, 25, 2, 0),
|
|
datetime.datetime(2018, 3, 25, 3, 0),
|
|
datetime.datetime(2018, 3, 25, 4, 0),
|
|
datetime.datetime(2018, 3, 25, 5, 0),
|
|
datetime.datetime(2018, 3, 25, 6, 0),
|
|
datetime.datetime(2018, 3, 25, 7, 0),
|
|
datetime.datetime(2018, 3, 25, 8, 0),
|
|
datetime.datetime(2018, 3, 25, 9, 0),
|
|
datetime.datetime(2018, 3, 25, 10, 0),
|
|
datetime.datetime(2018, 3, 25, 11, 0),
|
|
datetime.datetime(2018, 3, 25, 12, 0),
|
|
datetime.datetime(2018, 3, 25, 13, 0),
|
|
datetime.datetime(2018, 3, 25, 14, 0),
|
|
datetime.datetime(2018, 3, 25, 15, 0),
|
|
datetime.datetime(2018, 3, 25, 16, 0),
|
|
datetime.datetime(2018, 3, 25, 17, 0),
|
|
datetime.datetime(2018, 3, 25, 18, 0),
|
|
datetime.datetime(2018, 3, 25, 19, 0),
|
|
datetime.datetime(2018, 3, 25, 20, 0),
|
|
datetime.datetime(2018, 3, 25, 21, 0),
|
|
datetime.datetime(2018, 3, 25, 22, 0),
|
|
datetime.datetime(2018, 3, 25, 23, 0),
|
|
datetime.datetime(2018, 3, 26, 0, 0)
|
|
]
|
|
|
|
dates = [date for date in date_utils.date_range(start, end, step)]
|
|
|
|
self.assertEqual(dates, expected)
|
|
|
|
|
|
class TestFormatLangDate(TransactionCase):
|
|
def test_00_accepted_types(self):
|
|
self.env.user.tz = 'Europe/Brussels'
|
|
datetime_str = '2017-01-31 12:00:00'
|
|
date_datetime = datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
|
date_date = date_datetime.date()
|
|
date_str = '2017-01-31'
|
|
time_part = datetime.time(16, 30, 22)
|
|
t_medium = 'h:mm:ss a'
|
|
medium = f'MMM d, YYYY, {t_medium}'
|
|
|
|
self.assertEqual(misc.format_date(self.env, date_datetime), '01/31/2017')
|
|
self.assertEqual(misc.format_date(self.env, date_date), '01/31/2017')
|
|
self.assertEqual(misc.format_date(self.env, date_str), '01/31/2017')
|
|
self.assertEqual(misc.format_date(self.env, ''), '')
|
|
self.assertEqual(misc.format_date(self.env, False), '')
|
|
self.assertEqual(misc.format_date(self.env, None), '')
|
|
|
|
self.assertEqual(misc.format_datetime(self.env, date_datetime, dt_format=medium), 'Jan 31, 2017, 1:00:00 PM')
|
|
self.assertEqual(misc.format_datetime(self.env, datetime_str, dt_format=medium), 'Jan 31, 2017, 1:00:00 PM')
|
|
self.assertEqual(misc.format_datetime(self.env, '', dt_format=medium), '')
|
|
self.assertEqual(misc.format_datetime(self.env, False, dt_format=medium), '')
|
|
self.assertEqual(misc.format_datetime(self.env, None, dt_format=medium), '')
|
|
|
|
self.assertEqual(misc.format_time(self.env, time_part, time_format=t_medium), '4:30:22 PM')
|
|
self.assertEqual(misc.format_time(self.env, '', time_format=t_medium), '')
|
|
self.assertEqual(misc.format_time(self.env, False, time_format=t_medium), '')
|
|
self.assertEqual(misc.format_time(self.env, None, time_format=t_medium), '')
|
|
|
|
def test_01_code_and_format(self):
|
|
date_str = '2017-01-31'
|
|
lang = self.env['res.lang']
|
|
|
|
# Activate French and Simplified Chinese (test with non-ASCII characters)
|
|
lang._activate_lang('fr_FR')
|
|
lang._activate_lang('zh_CN')
|
|
|
|
# -- test `date`
|
|
# Change a single parameter
|
|
self.assertEqual(misc.format_date(lang.with_context(lang='fr_FR').env, date_str), '31/01/2017')
|
|
self.assertEqual(misc.format_date(lang.env, date_str, lang_code='fr_FR'), '31/01/2017')
|
|
self.assertEqual(misc.format_date(lang.env, date_str, date_format='MMM d, y'), 'Jan 31, 2017')
|
|
|
|
# Change 2 parameters
|
|
self.assertEqual(misc.format_date(lang.with_context(lang='zh_CN').env, date_str, lang_code='fr_FR'), '31/01/2017')
|
|
self.assertEqual(misc.format_date(lang.with_context(lang='zh_CN').env, date_str, date_format='MMM d, y'), u'1\u6708 31, 2017')
|
|
self.assertEqual(misc.format_date(lang.env, date_str, lang_code='fr_FR', date_format='MMM d, y'), 'janv. 31, 2017')
|
|
|
|
# Change 3 parameters
|
|
self.assertEqual(misc.format_date(lang.with_context(lang='zh_CN').env, date_str, lang_code='en_US', date_format='MMM d, y'), 'Jan 31, 2017')
|
|
|
|
# -- test `datetime`
|
|
datetime_str = '2017-01-31 10:33:00'
|
|
# Change languages and timezones
|
|
datetime_us_str = misc.format_datetime(lang.with_context(lang='en_US').env, datetime_str, tz='Europe/Brussels')
|
|
self.assertNotEqual(misc.format_datetime(lang.with_context(lang='fr_FR').env, datetime_str, tz='Europe/Brussels'), datetime_us_str)
|
|
self.assertNotEqual(misc.format_datetime(lang.with_context(lang='zh_CN').env, datetime_str, tz='America/New_York'), datetime_us_str)
|
|
|
|
# Change language, timezone and format
|
|
self.assertEqual(misc.format_datetime(lang.with_context(lang='fr_FR').env, datetime_str, tz='America/New_York', dt_format='dd/MM/YYYY HH:mm'), '31/01/2017 05:33')
|
|
self.assertEqual(misc.format_datetime(lang.with_context(lang='en_US').env, datetime_str, tz='Europe/Brussels', dt_format='MMM d, y'), 'Jan 31, 2017')
|
|
|
|
# Check given `lang_code` overwites context lang
|
|
fmt_fr = 'dd MMMM YYYY à HH:mm:ss Z'
|
|
fmt_us = "MMMM dd, YYYY 'at' hh:mm:ss a Z"
|
|
self.assertEqual(misc.format_datetime(lang.env, datetime_str, tz='Europe/Brussels', dt_format=fmt_fr, lang_code='fr_FR'), '31 janvier 2017 à 11:33:00 +0100')
|
|
self.assertEqual(misc.format_datetime(lang.with_context(lang='zh_CN').env, datetime_str, tz='Europe/Brussels', dt_format=fmt_us, lang_code='en_US'), 'January 31, 2017 at 11:33:00 AM +0100')
|
|
|
|
# -- test `time`
|
|
time_part = datetime.time(16, 30, 22)
|
|
time_part_tz = datetime.time(16, 30, 22, tzinfo=pytz.timezone('America/New_York')) # 4:30 PM timezoned
|
|
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, time_part, time_format='HH:mm:ss'), '16:30:22')
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='zh_CN').env, time_part, time_format="ah:m:ss"), '\u4e0b\u53484:30:22')
|
|
|
|
# Check format in different languages
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, time_part, time_format='HH:mm'), '16:30')
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='zh_CN').env, time_part, time_format='ah:mm'), '\u4e0b\u53484:30')
|
|
|
|
# Check timezoned time part
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, time_part_tz, time_format='HH:mm:ss Z'), '16:30:22 -0504')
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='zh_CN').env, time_part_tz, time_format='zzzz ah:mm:ss'), '\u5317\u7f8e\u4e1c\u90e8\u6807\u51c6\u65f6\u95f4\u0020\u4e0b\u53484:30:22')
|
|
|
|
#Check timezone conversion in format_time
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, datetime_str, 'Europe/Brussels', time_format='HH:mm:ss Z'), '11:33:00 +0100')
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, datetime_str, 'America/New_York', time_format='HH:mm:ss Z'), '05:33:00 -0500')
|
|
|
|
# Check given `lang_code` overwites context lang
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, time_part, time_format='ah:mm', lang_code='zh_CN'), '\u4e0b\u53484:30')
|
|
self.assertEqual(misc.format_time(lang.with_context(lang='zh_CN').env, time_part, time_format='ah:mm', lang_code='fr_FR'), 'PM4:30')
|
|
|
|
def test_02_tz(self):
|
|
self.env.user.tz = 'Europe/Brussels'
|
|
datetime_str = '2016-12-31 23:55:00'
|
|
date_datetime = datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
|
|
|
# While London is still in 2016, Brussels is already in 2017
|
|
self.assertEqual(misc.format_date(self.env, date_datetime), '01/01/2017')
|
|
|
|
# Force London timezone
|
|
date_datetime = date_datetime.replace(tzinfo=pytz.UTC)
|
|
self.assertEqual(misc.format_date(self.env, date_datetime), '12/31/2016', "User's tz must be ignored when tz is specifed in datetime object")
|
|
|
|
|
|
class TestCallbacks(BaseCase):
|
|
def test_callback(self):
|
|
log = []
|
|
callbacks = misc.Callbacks()
|
|
|
|
# add foo
|
|
def foo():
|
|
log.append("foo")
|
|
|
|
callbacks.add(foo)
|
|
|
|
# add bar
|
|
@callbacks.add
|
|
def bar():
|
|
log.append("bar")
|
|
|
|
# add foo again
|
|
callbacks.add(foo)
|
|
|
|
# this should call foo(), bar(), foo()
|
|
callbacks.run()
|
|
self.assertEqual(log, ["foo", "bar", "foo"])
|
|
|
|
# this should do nothing
|
|
callbacks.run()
|
|
self.assertEqual(log, ["foo", "bar", "foo"])
|
|
|
|
def test_aggregate(self):
|
|
log = []
|
|
callbacks = misc.Callbacks()
|
|
|
|
# register foo once
|
|
@callbacks.add
|
|
def foo():
|
|
log.append(callbacks.data["foo"])
|
|
|
|
# aggregate data
|
|
callbacks.data.setdefault("foo", []).append(1)
|
|
callbacks.data.setdefault("foo", []).append(2)
|
|
callbacks.data.setdefault("foo", []).append(3)
|
|
|
|
# foo() is called once
|
|
callbacks.run()
|
|
self.assertEqual(log, [[1, 2, 3]])
|
|
self.assertFalse(callbacks.data)
|
|
|
|
callbacks.run()
|
|
self.assertEqual(log, [[1, 2, 3]])
|
|
|
|
def test_reentrant(self):
|
|
log = []
|
|
callbacks = misc.Callbacks()
|
|
|
|
# register foo that runs callbacks
|
|
@callbacks.add
|
|
def foo():
|
|
log.append("foo1")
|
|
callbacks.run()
|
|
log.append("foo2")
|
|
|
|
@callbacks.add
|
|
def bar():
|
|
log.append("bar")
|
|
|
|
# both foo() and bar() are called once
|
|
callbacks.run()
|
|
self.assertEqual(log, ["foo1", "bar", "foo2"])
|
|
|
|
callbacks.run()
|
|
self.assertEqual(log, ["foo1", "bar", "foo2"])
|
|
|
|
|
|
class TestRemoveAccents(BaseCase):
|
|
def test_empty_string(self):
|
|
self.assertEqual(remove_accents(False), False)
|
|
self.assertEqual(remove_accents(''), '')
|
|
self.assertEqual(remove_accents(None), None)
|
|
|
|
def test_latin(self):
|
|
self.assertEqual(remove_accents('Niño Hernández'), 'Nino Hernandez')
|
|
self.assertEqual(remove_accents('Anaïs Clémence'), 'Anais Clemence')
|
|
|
|
def test_non_latin(self):
|
|
self.assertEqual(remove_accents('العربية'), 'العربية')
|
|
self.assertEqual(remove_accents('русский алфавит'), 'русскии алфавит')
|
|
|
|
|
|
class TestAddonsFileAccess(BaseCase):
|
|
|
|
def assertCannotAccess(self, path, ExceptionType=FileNotFoundError, filter_ext=None):
|
|
with self.assertRaises(ExceptionType):
|
|
file_path(path, filter_ext=filter_ext)
|
|
|
|
def assertCanRead(self, path, needle='', mode='r', filter_ext=None):
|
|
with file_open(path, mode, filter_ext) as f:
|
|
self.assertIn(needle, f.read())
|
|
|
|
def assertCannotRead(self, path, ExceptionType=FileNotFoundError, filter_ext=None):
|
|
with self.assertRaises(ExceptionType):
|
|
file_open(path, filter_ext=filter_ext)
|
|
|
|
def test_file_path(self):
|
|
# absolute path
|
|
self.assertEqual(__file__, file_path(__file__))
|
|
self.assertEqual(__file__, file_path(__file__, filter_ext=None)) # means "no filter" too
|
|
self.assertEqual(__file__, file_path(__file__, filter_ext=('.py',)))
|
|
|
|
# directory target is ok
|
|
self.assertEqual(os.path.dirname(__file__), file_path(os.path.join(__file__, '..')))
|
|
|
|
# relative path
|
|
relpath = os.path.join(*(__file__.split(os.sep)[-3:])) # 'base/tests/test_misc.py'
|
|
self.assertEqual(__file__, file_path(relpath))
|
|
self.assertEqual(__file__, file_path(relpath, filter_ext=('.py',)))
|
|
|
|
# leading 'addons/' is ignored if present
|
|
self.assertTrue(file_path("addons/web/__init__.py"))
|
|
relpath = os.path.join('addons', relpath) # 'addons/base/tests/test_misc.py'
|
|
self.assertEqual(__file__, file_path(relpath))
|
|
|
|
# files in root_path are allowed
|
|
self.assertTrue(file_path('tools/misc.py'))
|
|
|
|
# errors when outside addons_paths
|
|
self.assertCannotAccess('/doesnt/exist')
|
|
self.assertCannotAccess('/tmp')
|
|
self.assertCannotAccess('../../../../../../../../../tmp')
|
|
self.assertCannotAccess(os.path.join(__file__, '../../../../../'))
|
|
|
|
# data_dir is forbidden
|
|
self.assertCannotAccess(config['data_dir'])
|
|
|
|
# errors for illegal extensions
|
|
self.assertCannotAccess(__file__, ValueError, filter_ext=('.png',))
|
|
# file doesnt exist but has wrong extension
|
|
self.assertCannotAccess(__file__.replace('.py', '.foo'), ValueError, filter_ext=('.png',))
|
|
|
|
def test_file_open(self):
|
|
# The needle includes UTF8 so we test reading non-ASCII files at the same time.
|
|
# This depends on the system locale and is harder to unit test, but if you manage to run the
|
|
# test with a non-UTF8 locale (`LC_ALL=fr_FR.iso8859-1 python3...`) it should not crash ;-)
|
|
test_needle = "A needle with non-ascii bytes: ♥"
|
|
|
|
# absolute path
|
|
self.assertCanRead(__file__, test_needle)
|
|
self.assertCanRead(__file__, test_needle.encode(), mode='rb')
|
|
self.assertCanRead(__file__, test_needle.encode(), mode='rb', filter_ext=('.py',))
|
|
|
|
# directory target *is* an error
|
|
with self.assertRaises(FileNotFoundError):
|
|
file_open(os.path.join(__file__, '..'))
|
|
|
|
# relative path
|
|
relpath = os.path.join(*(__file__.split(os.sep)[-3:])) # 'base/tests/test_misc.py'
|
|
self.assertCanRead(relpath, test_needle)
|
|
self.assertCanRead(relpath, test_needle.encode(), mode='rb')
|
|
self.assertCanRead(relpath, test_needle.encode(), mode='rb', filter_ext=('.py',))
|
|
|
|
# leading 'addons/' is ignored if present
|
|
self.assertCanRead("addons/web/__init__.py", "import")
|
|
relpath = os.path.join('addons', relpath) # 'addons/base/tests/test_misc.py'
|
|
self.assertCanRead(relpath, test_needle)
|
|
|
|
# files in root_path are allowed
|
|
self.assertCanRead('tools/misc.py')
|
|
|
|
# errors when outside addons_paths
|
|
self.assertCannotRead('/doesnt/exist')
|
|
self.assertCannotRead('')
|
|
self.assertCannotRead('/tmp')
|
|
self.assertCannotRead('../../../../../../../../../tmp')
|
|
self.assertCannotRead(os.path.join(__file__, '../../../../../'))
|
|
|
|
# data_dir is forbidden
|
|
self.assertCannotRead(config['data_dir'])
|
|
|
|
# errors for illegal extensions
|
|
self.assertCannotRead(__file__, ValueError, filter_ext=('.png',))
|
|
# file doesnt exist but has wrong extension
|
|
self.assertCannotRead(__file__.replace('.py', '.foo'), ValueError, filter_ext=('.png',))
|
|
|
|
|
|
class TestDictTools(BaseCase):
|
|
def test_readonly_dict(self):
|
|
d = misc.ReadonlyDict({'foo': 'bar'})
|
|
with self.assertRaises(TypeError):
|
|
d['baz'] = 'xyz'
|
|
with self.assertRaises(AttributeError):
|
|
d.update({'baz': 'xyz'})
|
|
with self.assertRaises(TypeError):
|
|
dict.update(d, {'baz': 'xyz'})
|
|
|
|
|
|
class TestFormatLang(TransactionCase):
|
|
def test_value_and_digits(self):
|
|
self.assertEqual(misc.formatLang(self.env, 100.23, digits=1), '100.2')
|
|
self.assertEqual(misc.formatLang(self.env, 100.23, digits=3), '100.230')
|
|
|
|
self.assertEqual(misc.formatLang(self.env, ''), '', 'If value is an empty string, it should return an empty string (not 0)')
|
|
|
|
self.assertEqual(misc.formatLang(self.env, 100), '100.00', 'If digits is None (default value), it should default to 2')
|
|
|
|
# Default rounding is 'HALF_EVEN'
|
|
self.assertEqual(misc.formatLang(self.env, 100.205), '100.20')
|
|
self.assertEqual(misc.formatLang(self.env, 100.215), '100.22')
|
|
|
|
def test_grouping(self):
|
|
self.env["res.lang"].create({
|
|
"name": "formatLang Lang",
|
|
"code": "fLT",
|
|
"grouping": "[3,2,-1]",
|
|
"decimal_point": "!",
|
|
"thousands_sep": "?",
|
|
})
|
|
|
|
self.env['res.lang']._activate_lang('fLT')
|
|
|
|
self.assertEqual(misc.formatLang(self.env['res.lang'].with_context(lang='fLT').env, 1000000000, grouping=True), '10000?00?000!00')
|
|
self.assertEqual(misc.formatLang(self.env['res.lang'].with_context(lang='fLT').env, 1000000000, grouping=False), '1000000000.00')
|
|
|
|
def test_decimal_precision(self):
|
|
decimal_precision = self.env['decimal.precision'].create({
|
|
'name': 'formatLang Decimal Precision',
|
|
'digits': 3, # We want .001 decimals to make sure the decimal precision parameter 'dp' is chosen.
|
|
})
|
|
|
|
self.assertEqual(misc.formatLang(self.env, 100, dp=decimal_precision.name), '100.000')
|
|
|
|
def test_currency_object(self):
|
|
currency_object = self.env['res.currency'].create({
|
|
'name': 'formatLang Currency',
|
|
'symbol': 'fL',
|
|
'rounding': 0.1, # We want .1 decimals to make sure 'currency_obj' is chosen.
|
|
'position': 'after',
|
|
})
|
|
|
|
self.assertEqual(misc.formatLang(self.env, 100, currency_obj=currency_object), '100.0%sfL' % u'\N{NO-BREAK SPACE}')
|
|
|
|
currency_object.write({'position': 'before'})
|
|
|
|
self.assertEqual(misc.formatLang(self.env, 100, currency_obj=currency_object), 'fL%s100.0' % u'\N{NO-BREAK SPACE}')
|
|
|
|
def test_decimal_precision_and_currency_object(self):
|
|
decimal_precision = self.env['decimal.precision'].create({
|
|
'name': 'formatLang Decimal Precision',
|
|
'digits': 3,
|
|
})
|
|
|
|
currency_object = self.env['res.currency'].create({
|
|
'name': 'formatLang Currency',
|
|
'symbol': 'fL',
|
|
'rounding': 0.1,
|
|
'position': 'after',
|
|
})
|
|
|
|
# If we have a 'dp' and 'currency_obj', we use the decimal precision of 'dp' and the format of 'currency_obj'.
|
|
self.assertEqual(misc.formatLang(self.env, 100, dp=decimal_precision.name, currency_obj=currency_object), '100.000%sfL' % u'\N{NO-BREAK SPACE}')
|
|
|
|
def test_rounding_method(self):
|
|
self.assertEqual(misc.formatLang(self.env, 100.205), '100.20') # Default is 'HALF-EVEN'
|
|
self.assertEqual(misc.formatLang(self.env, 100.215), '100.22') # Default is 'HALF-EVEN'
|
|
|
|
self.assertEqual(misc.formatLang(self.env, 100.205, rounding_method='HALF-UP'), '100.21')
|
|
self.assertEqual(misc.formatLang(self.env, 100.215, rounding_method='HALF-UP'), '100.22')
|
|
|
|
self.assertEqual(misc.formatLang(self.env, 100.205, rounding_method='HALF-DOWN'), '100.20')
|
|
self.assertEqual(misc.formatLang(self.env, 100.215, rounding_method='HALF-DOWN'), '100.21')
|
|
|
|
def test_rounding_unit(self):
|
|
self.assertEqual(misc.formatLang(self.env, 1000000.00), '1,000,000.00')
|
|
self.assertEqual(misc.formatLang(self.env, 1000000.00, rounding_unit='units'), '1,000,000')
|
|
self.assertEqual(misc.formatLang(self.env, 1000000.00, rounding_unit='thousands'), '1,000')
|
|
self.assertEqual(misc.formatLang(self.env, 1000000.00, rounding_unit='lakhs'), '10')
|
|
self.assertEqual(misc.formatLang(self.env, 1000000.00, rounding_unit="millions"), '1')
|
|
|
|
def test_rounding_method_and_rounding_unit(self):
|
|
self.assertEqual(misc.formatLang(self.env, 1822060000, rounding_method='HALF-UP', rounding_unit='lakhs'), '18,221')
|
|
self.assertEqual(misc.formatLang(self.env, 1822050000, rounding_method='HALF-UP', rounding_unit='lakhs'), '18,221')
|
|
self.assertEqual(misc.formatLang(self.env, 1822049900, rounding_method='HALF-UP', rounding_unit='lakhs'), '18,220')
|
|
|
|
|
|
class TestUrlValidate(BaseCase):
|
|
def test_url_validate(self):
|
|
for case, truth in [
|
|
# full URLs should be preserved
|
|
('http://example.com', 'http://example.com'),
|
|
('http://example.com/index.html', 'http://example.com/index.html'),
|
|
('http://example.com?debug=1', 'http://example.com?debug=1'),
|
|
('http://example.com#h3', 'http://example.com#h3'),
|
|
|
|
# URLs with a domain should get a http scheme
|
|
('example.com', 'http://example.com'),
|
|
('example.com/index.html', 'http://example.com/index.html'),
|
|
('example.com?debug=1', 'http://example.com?debug=1'),
|
|
('example.com#h3', 'http://example.com#h3'),
|
|
]:
|
|
with self.subTest(case=case):
|
|
self.assertEqual(validate_url(case), truth)
|
|
|
|
# broken cases, do we really want that?
|
|
self.assertEqual(validate_url('/index.html'), 'http:///index.html')
|
|
self.assertEqual(validate_url('?debug=1'), 'http://?debug=1')
|
|
self.assertEqual(validate_url('#model=project.task&id=3603607'), 'http://#model=project.task&id=3603607')
|