1import unittest
2from unittest.mock import mock_open, patch
3from secret import validate_recipe, RuinedNikuldenDinnerError
4
5def memoization(func): # Decorator to cache results of expensive function calls
6 data = {}
7
8 def wrapper(*args):
9 if args in data:
10 return data[args]
11 result = func(*args)
12 data[args] = result
13 return result
14
15 return wrapper
16
17@memoization
18def generate_all_variations(word):
19 size = len(word)
20 result = []
21 for i in range(1 << size): # Loop through all possible bitmasks for the word
22 current = []
23 for j in range(size): # Loop through all letters (their binary representation)
24 if i & (1 << j): # Check if the j-th bit is 1 (is upper)
25 current.append(word[j].upper())
26 else:
27 current.append(word[j].lower())
28 result.append(''.join(current))
29 return result
30
31@memoization
32def generate_all_insertions_for_keyword(word, special_word):
33 # Generate all insertions of special_word into the given word
34 return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)]
35
36@memoization
37def generate_all_insertions_for_random(word, special_word):
38 # Generate all insertions of word into the given special_word
39 return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)]
40
41
42class TestNikuldenValidator(unittest.TestCase):
43
44 def setUp(self):
45 self.keywords = ["риба", "рибена", "шаран", "сьонга"]
46 self.special_word = "тр"
47 self.valid_keywords = set()
48 self.invalid_keywords = set()
49 self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","]
50
51 self.templates = [
52 "Днес ще ям {keyword}, защото е Никулден ;Д.",
53 "Тази рецепта включва {keyword}.",
54 "{keyword} е подходящо ястие за Никулден.",
55 "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.",
56 "{keyword} е доста екзотично и полезно ястие.",
57 "Практически {keyword} е клас {keyword} нали???",
58 "{keyword}",
59 "{keyword} ",
60 " {keyword}",
61 " {keyword} "
62 ]
63
64 for current_keyword in self.keywords:
65 variations = generate_all_variations(current_keyword)
66 self.valid_keywords.update(variations)
67
68 def test_valid_recipe(self):
69 valid_contents = [
70 template.format(keyword=current_keyword)
71 for current_keyword in self.valid_keywords
72 for template in self.templates
73 ]
74
75 for content in valid_contents:
76 with self.subTest(content=content):
77 m = mock_open(read_data=content)
78 with patch("builtins.open", m):
79 result = validate_recipe("dummy_path.txt")
80 self.assertTrue(result, "Error")
81
82 def test_invalid_recipe(self):
83 for current_keyword in self.valid_keywords:
84 variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word)
85 variations_random = generate_all_insertions_for_random(current_keyword, self.special_word)
86 self.invalid_keywords.update(variations_keyword)
87 self.invalid_keywords.update(variations_random)
88
89 invalid_contents = self.some_edge_cases.copy() # self.some_edge_cases is mutable
90
91 invalid_contents.extend(
92 template.format(keyword=current_keyword)
93 for current_keyword in self.invalid_keywords
94 for template in self.templates
95 )
96
97 for content in invalid_contents:
98 with self.subTest(content=content):
99 m = mock_open(read_data=content)
100 with patch("builtins.open", m):
101 result = validate_recipe("dummy_path.txt")
102 self.assertFalse(result, "Error")
103
104 def test_bad_recipe_file(self):
105 error_cases = [OSError, IOError]
106
107 for error in error_cases:
108 with self.subTest(error=error):
109 with patch("builtins.open", side_effect=error):
110 with self.assertRaises(RuinedNikuldenDinnerError):
111 validate_recipe("missing_file.txt")
112
113if __name__ == "__main__":
114 unittest.main()
Timed out.
Виктор Бечев
07.12.2024 13:30Решението ти е доста изчерпателно, дори прекалено...
Тестовете ти вървят ~22 секунди на моята машина, а нашите тестове, които тестват вашите тестове с различни имплементации на `validate_recipe` - минута и половина.
Да не се опитвам да перифразирам, ето коментар, който оставих в друго решение:
```
Можеш да напишеш нещо сравнително кратко, което да ти прави въпросните пермутации, но истината е, че не е нужно.
Алтернативен пример - ако имаме функция, която повдига четни числа на втора степен, нечетни - на трета и работи в отворен интервал от 0 до 100. Имаме да тестване за:
Четно число.
Нечетно число.
Гранични стойности - 0 и 100.
Не е нужно да тестваме с 2, 4, 6, 8 и т.н., достатъчно е да тестваме с едно четно число. Същото важи и за нечетните. Не е нужно да тестваме и за -5 и -500, достатъчно е да изтестваме поведението в граничните стойности - 0 и 100.
Философията тук е, че да, винаги можеш да напишеш функция, която се чупи точно на 87.
Но идеята не е да тестваме функциите, които пишем с всеки възможен input, а да генерализираме тестовете си спрямо изискванията, така че да могат да ни дадат достатъчно добра увереност, че функцията изпълнява дефинираните изисквания.
Защото този проблем, който дефинирах е с елементарна сложност, но ако функцията ти е по-сложна или дори със същата сложност, но има граници от -сто милиона до +сто милиона - ще тестваш ли 200 000 002 пъти?
Пък и да не забравяме невалидните стойности - те са безкрайно множество.
```
|
Йоан Байчев
05.12.2024 18:09Като заключение бих казал, че моето решение се интересува от поведението на "правилните" думи ако те съществуват във файла, намиращи се около или в някоя част на друга дума, която реално няма значение стига да може да симулира 3-те случая (префикс, инфикс и суфикс), тоест да е поне 2-буквена.
|
Йоан Байчев
05.12.2024 18:05Ако разгледаме думата "риба", тогава:
self.valid_keywords = {'рибА', 'риба', 'Риба', 'РИба', 'рИба', 'РиБА', 'рИБа', 'рИбА', 'риБа', 'РибА', 'рИБА', 'РИБА', 'РИбА', 'риБА', 'РИБа', 'РиБа'}
self.invalid_keywords = {'РИБатр', 'рибАтр', 'рИбтра', 'РтриБА', 'трИБАр', 'трибАр', 'РитрБа', 'РИбАтр', 'ртриБА', 'рИБтра', 'рибтра', 'трРиБа', 'ртрИба', 'тРибАр', 'трРИба', 'трРиба', 'риБАтр', 'РИбатр', 'рИБатр', 'РиБтра', 'тРИбАр', 'рИбтрА', 'ртрИбА', 'рИбатр', 'тррИбА', 'рИтрБа', 'РиБатр', 'РИбтрА', 'ритрба', 'РтрибА', 'РИБтра', 'трИБар', 'триБАр', 'Ртриба', 'тРИбар', 'рИбАтр', 'РтрИБА', 'тррибА', 'РИтрБа', 'ритрбА', 'риБтра', 'трРИБа', 'рИтрбА', 'тррИба', 'трИбАр', 'РтрИбА', 'РибтрА', 'РИбтра', 'РИБАтр', 'РиБАтр', 'рИБАтр', 'ртриба', 'трРибА', 'трРиБА', 'РтрИБа', 'тРИБАр', 'РИтрбА', 'рибтрА', 'трриБа', 'РИБтрА', 'тРиБар', 'рИтрБА', 'тРибар', 'ритрБа', 'ртрИБА', 'РитрбА', 'тррИБа', 'трИбар', 'Ритрба', 'РиБтрА', 'ритрБА', 'рИБтрА', 'трриба', 'тррИБА', 'рибатр', 'ртрибА', 'РтрИба', 'тРиБАр', 'трибар', 'РибАтр', 'ртрИБа', 'РтриБа', 'триБар', 'РитрБА', 'Рибатр', 'риБтрА', 'трРИБА', 'ртриБа', 'трриБА', 'РИтрБА', 'Рибтра', 'рИтрба', 'РИтрба', 'трРИбА', 'риБатр', 'тРИБар'}
|
Йоан Байчев
05.12.2024 17:511. Функцията generate_all_variations реално прави това тор -> {тор, Тор, тОр, тоР, ТОр, тОР, ТоР, ТОР}, генерира всички възможни думи, които ще върнат истина, като нали съответно тор е някоя от {"риба", "рибена", "шаран", "сьонга"}. Логиката ми е такава, на главна буква съпоставям 1 и на малка съпоставям 0, тоест с пример ТОР = 111, тоР = 001 или цялото би изглеждало така тор -> {000, 100, 010, 001, 110, 011, 101, 111} тоест за всяка буква в тор имаме две възможности 1 или 0 и тъй като тор има 3 букви следователно трябва да имаме 2^3 = 8 възможности, оттук идва логиката с for i in range(1 << size), тъй като 1 << size = 2 ^ size. С условието if i & (1 << j) проверяваме дали битът на позиция j в числото i е вдигнат (тоест е 1 ) и буквата е главна, като при примера с тор, i е някое число от {000, 100, 010, 001, 110, 011, 101, 111}, а j е съответно някое позиция от {0, 1, 2}, и съответно ако е 0 е малка буква.
2. Функцията generate_all_insertions_for_keyword генерира префикс, инфикс, суфикс, като word е някоя от думите {"риба", "рибена", "шаран", "сьонга"}, като е важно да се отбележи, че имам "константа" дума special_word, която така да го кажем винаги стои в средата.
3. Функцията generate_all_insertions_for_random генерира префикс, инфикс, суфикс, като word е някоя от думите {"риба", "рибена", "шаран", "сьонга"}, като тук "константата" дума е word, а special_word я обгръща(подобно на миналото, но на обратно)
Изборът ми на special_word е "тр", мотивацията ми е че просто искам поне 2-буквена дума заради 3-те случая префикс, инфикс, суфикс и възможно най-малко сметки!
4. Функцията memoization е очевидна. В дадената задача с {"риба", "рибена", "шаран", "сьонга"} няма смисъл, но по-късна обработка и при добавяне на повтарящи се думи, било то умишлено или по грешка, това ще действа автоматизирано и ще ги "пренебрегва", като спестява време. Също така, ако по-късно към кода се добави допълнителна логика, която многократно извиква тези функции със същите параметри, кеширането ще спести време.
Добавил съм some_edge_cases за празен файл, табулация, нов ред и други, като частни случаи, който може да бъде разширяван във времето. Създадох и templates, като удобен начин за добавяне на допълнителни тестове, без да се променя логиката на кода.
|
f | 1 | import unittest | f | 1 | import unittest |
2 | from unittest.mock import mock_open, patch | 2 | from unittest.mock import mock_open, patch | ||
3 | from secret import validate_recipe, RuinedNikuldenDinnerError | 3 | from secret import validate_recipe, RuinedNikuldenDinnerError | ||
4 | 4 | ||||
5 | def memoization(func): # Decorator to cache results of expensive function calls | 5 | def memoization(func): # Decorator to cache results of expensive function calls | ||
6 | data = {} | 6 | data = {} | ||
7 | 7 | ||||
8 | def wrapper(*args): | 8 | def wrapper(*args): | ||
9 | if args in data: | 9 | if args in data: | ||
10 | return data[args] | 10 | return data[args] | ||
11 | result = func(*args) | 11 | result = func(*args) | ||
12 | data[args] = result | 12 | data[args] = result | ||
13 | return result | 13 | return result | ||
14 | 14 | ||||
15 | return wrapper | 15 | return wrapper | ||
16 | 16 | ||||
17 | @memoization | 17 | @memoization | ||
18 | def generate_all_variations(word): | 18 | def generate_all_variations(word): | ||
19 | size = len(word) | 19 | size = len(word) | ||
20 | result = [] | 20 | result = [] | ||
21 | for i in range(1 << size): # Loop through all possible bitmasks for the word | 21 | for i in range(1 << size): # Loop through all possible bitmasks for the word | ||
22 | current = [] | 22 | current = [] | ||
23 | for j in range(size): # Loop through all letters (their binary representation) | 23 | for j in range(size): # Loop through all letters (their binary representation) | ||
24 | if i & (1 << j): # Check if the j-th bit is 1 (is upper) | 24 | if i & (1 << j): # Check if the j-th bit is 1 (is upper) | ||
25 | current.append(word[j].upper()) | 25 | current.append(word[j].upper()) | ||
26 | else: | 26 | else: | ||
27 | current.append(word[j].lower()) | 27 | current.append(word[j].lower()) | ||
28 | result.append(''.join(current)) | 28 | result.append(''.join(current)) | ||
29 | return result | 29 | return result | ||
30 | 30 | ||||
31 | @memoization | 31 | @memoization | ||
32 | def generate_all_insertions_for_keyword(word, special_word): | 32 | def generate_all_insertions_for_keyword(word, special_word): | ||
33 | # Generate all insertions of special_word into the given word | 33 | # Generate all insertions of special_word into the given word | ||
34 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | 34 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | ||
35 | 35 | ||||
36 | @memoization | 36 | @memoization | ||
37 | def generate_all_insertions_for_random(word, special_word): | 37 | def generate_all_insertions_for_random(word, special_word): | ||
38 | # Generate all insertions of word into the given special_word | 38 | # Generate all insertions of word into the given special_word | ||
39 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | 39 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | ||
40 | 40 | ||||
41 | 41 | ||||
42 | class TestNikuldenValidator(unittest.TestCase): | 42 | class TestNikuldenValidator(unittest.TestCase): | ||
43 | 43 | ||||
44 | def setUp(self): | 44 | def setUp(self): | ||
45 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | 45 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | ||
46 | self.special_word = "тр" | 46 | self.special_word = "тр" | ||
47 | self.valid_keywords = set() | 47 | self.valid_keywords = set() | ||
48 | self.invalid_keywords = set() | 48 | self.invalid_keywords = set() | ||
49 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | 49 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | ||
50 | 50 | ||||
51 | self.templates = [ | 51 | self.templates = [ | ||
52 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | 52 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | ||
53 | "Тази рецепта включва {keyword}.", | 53 | "Тази рецепта включва {keyword}.", | ||
54 | "{keyword} е подходящо ястие за Никулден.", | 54 | "{keyword} е подходящо ястие за Никулден.", | ||
55 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | 55 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | ||
t | 56 | "{keyword} е доста екзотично и полезно ястие." | t | 56 | "{keyword} е доста екзотично и полезно ястие.", |
57 | "Практически {keyword} е клас {keyword} нали???" | 57 | "Практически {keyword} е клас {keyword} нали???", | ||
58 | "{keyword}" | 58 | "{keyword}", | ||
59 | "{keyword} " | 59 | "{keyword} ", | ||
60 | " {keyword}" | 60 | " {keyword}", | ||
61 | " {keyword} " | 61 | " {keyword} " | ||
62 | ] | 62 | ] | ||
63 | 63 | ||||
64 | for current_keyword in self.keywords: | 64 | for current_keyword in self.keywords: | ||
65 | variations = generate_all_variations(current_keyword) | 65 | variations = generate_all_variations(current_keyword) | ||
66 | self.valid_keywords.update(variations) | 66 | self.valid_keywords.update(variations) | ||
67 | 67 | ||||
68 | def test_valid_recipe(self): | 68 | def test_valid_recipe(self): | ||
69 | valid_contents = [ | 69 | valid_contents = [ | ||
70 | template.format(keyword=current_keyword) | 70 | template.format(keyword=current_keyword) | ||
71 | for current_keyword in self.valid_keywords | 71 | for current_keyword in self.valid_keywords | ||
72 | for template in self.templates | 72 | for template in self.templates | ||
73 | ] | 73 | ] | ||
74 | 74 | ||||
75 | for content in valid_contents: | 75 | for content in valid_contents: | ||
76 | with self.subTest(content=content): | 76 | with self.subTest(content=content): | ||
77 | m = mock_open(read_data=content) | 77 | m = mock_open(read_data=content) | ||
78 | with patch("builtins.open", m): | 78 | with patch("builtins.open", m): | ||
79 | result = validate_recipe("dummy_path.txt") | 79 | result = validate_recipe("dummy_path.txt") | ||
80 | self.assertTrue(result, "Error") | 80 | self.assertTrue(result, "Error") | ||
81 | 81 | ||||
82 | def test_invalid_recipe(self): | 82 | def test_invalid_recipe(self): | ||
83 | for current_keyword in self.valid_keywords: | 83 | for current_keyword in self.valid_keywords: | ||
84 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | 84 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | ||
85 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | 85 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | ||
86 | self.invalid_keywords.update(variations_keyword) | 86 | self.invalid_keywords.update(variations_keyword) | ||
87 | self.invalid_keywords.update(variations_random) | 87 | self.invalid_keywords.update(variations_random) | ||
88 | 88 | ||||
89 | invalid_contents = self.some_edge_cases.copy() # self.some_edge_cases is mutable | 89 | invalid_contents = self.some_edge_cases.copy() # self.some_edge_cases is mutable | ||
90 | 90 | ||||
91 | invalid_contents.extend( | 91 | invalid_contents.extend( | ||
92 | template.format(keyword=current_keyword) | 92 | template.format(keyword=current_keyword) | ||
93 | for current_keyword in self.invalid_keywords | 93 | for current_keyword in self.invalid_keywords | ||
94 | for template in self.templates | 94 | for template in self.templates | ||
95 | ) | 95 | ) | ||
96 | 96 | ||||
97 | for content in invalid_contents: | 97 | for content in invalid_contents: | ||
98 | with self.subTest(content=content): | 98 | with self.subTest(content=content): | ||
99 | m = mock_open(read_data=content) | 99 | m = mock_open(read_data=content) | ||
100 | with patch("builtins.open", m): | 100 | with patch("builtins.open", m): | ||
101 | result = validate_recipe("dummy_path.txt") | 101 | result = validate_recipe("dummy_path.txt") | ||
102 | self.assertFalse(result, "Error") | 102 | self.assertFalse(result, "Error") | ||
103 | 103 | ||||
104 | def test_bad_recipe_file(self): | 104 | def test_bad_recipe_file(self): | ||
105 | error_cases = [OSError, IOError] | 105 | error_cases = [OSError, IOError] | ||
106 | 106 | ||||
107 | for error in error_cases: | 107 | for error in error_cases: | ||
108 | with self.subTest(error=error): | 108 | with self.subTest(error=error): | ||
109 | with patch("builtins.open", side_effect=error): | 109 | with patch("builtins.open", side_effect=error): | ||
110 | with self.assertRaises(RuinedNikuldenDinnerError): | 110 | with self.assertRaises(RuinedNikuldenDinnerError): | ||
111 | validate_recipe("missing_file.txt") | 111 | validate_recipe("missing_file.txt") | ||
112 | 112 | ||||
113 | if __name__ == "__main__": | 113 | if __name__ == "__main__": | ||
114 | unittest.main() | 114 | unittest.main() |
Legends | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
|
f | 1 | import unittest | f | 1 | import unittest |
2 | from unittest.mock import mock_open, patch | 2 | from unittest.mock import mock_open, patch | ||
3 | from secret import validate_recipe, RuinedNikuldenDinnerError | 3 | from secret import validate_recipe, RuinedNikuldenDinnerError | ||
4 | 4 | ||||
5 | def memoization(func): # Decorator to cache results of expensive function calls | 5 | def memoization(func): # Decorator to cache results of expensive function calls | ||
6 | data = {} | 6 | data = {} | ||
7 | 7 | ||||
8 | def wrapper(*args): | 8 | def wrapper(*args): | ||
9 | if args in data: | 9 | if args in data: | ||
10 | return data[args] | 10 | return data[args] | ||
11 | result = func(*args) | 11 | result = func(*args) | ||
12 | data[args] = result | 12 | data[args] = result | ||
13 | return result | 13 | return result | ||
14 | 14 | ||||
15 | return wrapper | 15 | return wrapper | ||
16 | 16 | ||||
17 | @memoization | 17 | @memoization | ||
18 | def generate_all_variations(word): | 18 | def generate_all_variations(word): | ||
19 | size = len(word) | 19 | size = len(word) | ||
20 | result = [] | 20 | result = [] | ||
21 | for i in range(1 << size): # Loop through all possible bitmasks for the word | 21 | for i in range(1 << size): # Loop through all possible bitmasks for the word | ||
22 | current = [] | 22 | current = [] | ||
23 | for j in range(size): # Loop through all letters (their binary representation) | 23 | for j in range(size): # Loop through all letters (their binary representation) | ||
24 | if i & (1 << j): # Check if the j-th bit is 1 (is upper) | 24 | if i & (1 << j): # Check if the j-th bit is 1 (is upper) | ||
25 | current.append(word[j].upper()) | 25 | current.append(word[j].upper()) | ||
26 | else: | 26 | else: | ||
27 | current.append(word[j].lower()) | 27 | current.append(word[j].lower()) | ||
28 | result.append(''.join(current)) | 28 | result.append(''.join(current)) | ||
29 | return result | 29 | return result | ||
30 | 30 | ||||
31 | @memoization | 31 | @memoization | ||
32 | def generate_all_insertions_for_keyword(word, special_word): | 32 | def generate_all_insertions_for_keyword(word, special_word): | ||
33 | # Generate all insertions of special_word into the given word | 33 | # Generate all insertions of special_word into the given word | ||
34 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | 34 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | ||
35 | 35 | ||||
36 | @memoization | 36 | @memoization | ||
37 | def generate_all_insertions_for_random(word, special_word): | 37 | def generate_all_insertions_for_random(word, special_word): | ||
38 | # Generate all insertions of word into the given special_word | 38 | # Generate all insertions of word into the given special_word | ||
39 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | 39 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | ||
40 | 40 | ||||
41 | 41 | ||||
42 | class TestNikuldenValidator(unittest.TestCase): | 42 | class TestNikuldenValidator(unittest.TestCase): | ||
43 | 43 | ||||
44 | def setUp(self): | 44 | def setUp(self): | ||
45 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | 45 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | ||
46 | self.special_word = "тр" | 46 | self.special_word = "тр" | ||
47 | self.valid_keywords = set() | 47 | self.valid_keywords = set() | ||
48 | self.invalid_keywords = set() | 48 | self.invalid_keywords = set() | ||
49 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | 49 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | ||
50 | 50 | ||||
51 | self.templates = [ | 51 | self.templates = [ | ||
52 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | 52 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | ||
53 | "Тази рецепта включва {keyword}.", | 53 | "Тази рецепта включва {keyword}.", | ||
54 | "{keyword} е подходящо ястие за Никулден.", | 54 | "{keyword} е подходящо ястие за Никулден.", | ||
55 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | 55 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | ||
56 | "{keyword} е доста екзотично и полезно ястие." | 56 | "{keyword} е доста екзотично и полезно ястие." | ||
57 | "Практически {keyword} е клас {keyword} нали???" | 57 | "Практически {keyword} е клас {keyword} нали???" | ||
t | t | 58 | "{keyword}" | ||
59 | "{keyword} " | ||||
60 | " {keyword}" | ||||
61 | " {keyword} " | ||||
58 | ] | 62 | ] | ||
59 | 63 | ||||
60 | for current_keyword in self.keywords: | 64 | for current_keyword in self.keywords: | ||
61 | variations = generate_all_variations(current_keyword) | 65 | variations = generate_all_variations(current_keyword) | ||
62 | self.valid_keywords.update(variations) | 66 | self.valid_keywords.update(variations) | ||
63 | 67 | ||||
64 | def test_valid_recipe(self): | 68 | def test_valid_recipe(self): | ||
65 | valid_contents = [ | 69 | valid_contents = [ | ||
66 | template.format(keyword=current_keyword) | 70 | template.format(keyword=current_keyword) | ||
67 | for current_keyword in self.valid_keywords | 71 | for current_keyword in self.valid_keywords | ||
68 | for template in self.templates | 72 | for template in self.templates | ||
69 | ] | 73 | ] | ||
70 | 74 | ||||
71 | for content in valid_contents: | 75 | for content in valid_contents: | ||
72 | with self.subTest(content=content): | 76 | with self.subTest(content=content): | ||
73 | m = mock_open(read_data=content) | 77 | m = mock_open(read_data=content) | ||
74 | with patch("builtins.open", m): | 78 | with patch("builtins.open", m): | ||
75 | result = validate_recipe("dummy_path.txt") | 79 | result = validate_recipe("dummy_path.txt") | ||
76 | self.assertTrue(result, "Error") | 80 | self.assertTrue(result, "Error") | ||
77 | 81 | ||||
78 | def test_invalid_recipe(self): | 82 | def test_invalid_recipe(self): | ||
79 | for current_keyword in self.valid_keywords: | 83 | for current_keyword in self.valid_keywords: | ||
80 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | 84 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | ||
81 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | 85 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | ||
82 | self.invalid_keywords.update(variations_keyword) | 86 | self.invalid_keywords.update(variations_keyword) | ||
83 | self.invalid_keywords.update(variations_random) | 87 | self.invalid_keywords.update(variations_random) | ||
84 | 88 | ||||
85 | invalid_contents = self.some_edge_cases.copy() # self.some_edge_cases is mutable | 89 | invalid_contents = self.some_edge_cases.copy() # self.some_edge_cases is mutable | ||
86 | 90 | ||||
87 | invalid_contents.extend( | 91 | invalid_contents.extend( | ||
88 | template.format(keyword=current_keyword) | 92 | template.format(keyword=current_keyword) | ||
89 | for current_keyword in self.invalid_keywords | 93 | for current_keyword in self.invalid_keywords | ||
90 | for template in self.templates | 94 | for template in self.templates | ||
91 | ) | 95 | ) | ||
92 | 96 | ||||
93 | for content in invalid_contents: | 97 | for content in invalid_contents: | ||
94 | with self.subTest(content=content): | 98 | with self.subTest(content=content): | ||
95 | m = mock_open(read_data=content) | 99 | m = mock_open(read_data=content) | ||
96 | with patch("builtins.open", m): | 100 | with patch("builtins.open", m): | ||
97 | result = validate_recipe("dummy_path.txt") | 101 | result = validate_recipe("dummy_path.txt") | ||
98 | self.assertFalse(result, "Error") | 102 | self.assertFalse(result, "Error") | ||
99 | 103 | ||||
100 | def test_bad_recipe_file(self): | 104 | def test_bad_recipe_file(self): | ||
101 | error_cases = [OSError, IOError] | 105 | error_cases = [OSError, IOError] | ||
102 | 106 | ||||
103 | for error in error_cases: | 107 | for error in error_cases: | ||
104 | with self.subTest(error=error): | 108 | with self.subTest(error=error): | ||
105 | with patch("builtins.open", side_effect=error): | 109 | with patch("builtins.open", side_effect=error): | ||
106 | with self.assertRaises(RuinedNikuldenDinnerError): | 110 | with self.assertRaises(RuinedNikuldenDinnerError): | ||
107 | validate_recipe("missing_file.txt") | 111 | validate_recipe("missing_file.txt") | ||
108 | 112 | ||||
109 | if __name__ == "__main__": | 113 | if __name__ == "__main__": | ||
110 | unittest.main() | 114 | unittest.main() |
Legends | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
|
f | 1 | import unittest | f | 1 | import unittest |
2 | from unittest.mock import mock_open, patch | 2 | from unittest.mock import mock_open, patch | ||
3 | from secret import validate_recipe, RuinedNikuldenDinnerError | 3 | from secret import validate_recipe, RuinedNikuldenDinnerError | ||
4 | 4 | ||||
n | 5 | def memoization(func): | n | 5 | def memoization(func): # Decorator to cache results of expensive function calls |
6 | data = {} | 6 | data = {} | ||
7 | 7 | ||||
8 | def wrapper(*args): | 8 | def wrapper(*args): | ||
9 | if args in data: | 9 | if args in data: | ||
10 | return data[args] | 10 | return data[args] | ||
11 | result = func(*args) | 11 | result = func(*args) | ||
12 | data[args] = result | 12 | data[args] = result | ||
13 | return result | 13 | return result | ||
14 | 14 | ||||
15 | return wrapper | 15 | return wrapper | ||
16 | 16 | ||||
17 | @memoization | 17 | @memoization | ||
18 | def generate_all_variations(word): | 18 | def generate_all_variations(word): | ||
19 | size = len(word) | 19 | size = len(word) | ||
20 | result = [] | 20 | result = [] | ||
n | 21 | for i in range(1 << size): | n | 21 | for i in range(1 << size): # Loop through all possible bitmasks for the word |
22 | current = [] | 22 | current = [] | ||
n | 23 | for j in range(size): | n | 23 | for j in range(size): # Loop through all letters (their binary representation) |
24 | if i & (1 << j): | 24 | if i & (1 << j): # Check if the j-th bit is 1 (is upper) | ||
25 | current.append(word[j].upper()) | 25 | current.append(word[j].upper()) | ||
26 | else: | 26 | else: | ||
27 | current.append(word[j].lower()) | 27 | current.append(word[j].lower()) | ||
28 | result.append(''.join(current)) | 28 | result.append(''.join(current)) | ||
29 | return result | 29 | return result | ||
30 | 30 | ||||
31 | @memoization | 31 | @memoization | ||
32 | def generate_all_insertions_for_keyword(word, special_word): | 32 | def generate_all_insertions_for_keyword(word, special_word): | ||
n | n | 33 | # Generate all insertions of special_word into the given word | ||
33 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | 34 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | ||
34 | 35 | ||||
35 | @memoization | 36 | @memoization | ||
36 | def generate_all_insertions_for_random(word, special_word): | 37 | def generate_all_insertions_for_random(word, special_word): | ||
n | n | 38 | # Generate all insertions of word into the given special_word | ||
37 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | 39 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | ||
38 | 40 | ||||
39 | 41 | ||||
40 | class TestNikuldenValidator(unittest.TestCase): | 42 | class TestNikuldenValidator(unittest.TestCase): | ||
41 | 43 | ||||
42 | def setUp(self): | 44 | def setUp(self): | ||
43 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | 45 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | ||
44 | self.special_word = "тр" | 46 | self.special_word = "тр" | ||
45 | self.valid_keywords = set() | 47 | self.valid_keywords = set() | ||
46 | self.invalid_keywords = set() | 48 | self.invalid_keywords = set() | ||
47 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | 49 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | ||
48 | 50 | ||||
49 | self.templates = [ | 51 | self.templates = [ | ||
50 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | 52 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | ||
51 | "Тази рецепта включва {keyword}.", | 53 | "Тази рецепта включва {keyword}.", | ||
52 | "{keyword} е подходящо ястие за Никулден.", | 54 | "{keyword} е подходящо ястие за Никулден.", | ||
53 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | 55 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | ||
54 | "{keyword} е доста екзотично и полезно ястие." | 56 | "{keyword} е доста екзотично и полезно ястие." | ||
55 | "Практически {keyword} е клас {keyword} нали???" | 57 | "Практически {keyword} е клас {keyword} нали???" | ||
56 | ] | 58 | ] | ||
57 | 59 | ||||
58 | for current_keyword in self.keywords: | 60 | for current_keyword in self.keywords: | ||
59 | variations = generate_all_variations(current_keyword) | 61 | variations = generate_all_variations(current_keyword) | ||
60 | self.valid_keywords.update(variations) | 62 | self.valid_keywords.update(variations) | ||
61 | 63 | ||||
62 | def test_valid_recipe(self): | 64 | def test_valid_recipe(self): | ||
63 | valid_contents = [ | 65 | valid_contents = [ | ||
64 | template.format(keyword=current_keyword) | 66 | template.format(keyword=current_keyword) | ||
65 | for current_keyword in self.valid_keywords | 67 | for current_keyword in self.valid_keywords | ||
66 | for template in self.templates | 68 | for template in self.templates | ||
67 | ] | 69 | ] | ||
68 | 70 | ||||
69 | for content in valid_contents: | 71 | for content in valid_contents: | ||
70 | with self.subTest(content=content): | 72 | with self.subTest(content=content): | ||
71 | m = mock_open(read_data=content) | 73 | m = mock_open(read_data=content) | ||
72 | with patch("builtins.open", m): | 74 | with patch("builtins.open", m): | ||
73 | result = validate_recipe("dummy_path.txt") | 75 | result = validate_recipe("dummy_path.txt") | ||
74 | self.assertTrue(result, "Error") | 76 | self.assertTrue(result, "Error") | ||
75 | 77 | ||||
76 | def test_invalid_recipe(self): | 78 | def test_invalid_recipe(self): | ||
77 | for current_keyword in self.valid_keywords: | 79 | for current_keyword in self.valid_keywords: | ||
78 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | 80 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | ||
79 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | 81 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | ||
80 | self.invalid_keywords.update(variations_keyword) | 82 | self.invalid_keywords.update(variations_keyword) | ||
81 | self.invalid_keywords.update(variations_random) | 83 | self.invalid_keywords.update(variations_random) | ||
82 | 84 | ||||
t | 83 | invalid_contents = self.some_edge_cases.copy() | t | 85 | invalid_contents = self.some_edge_cases.copy() # self.some_edge_cases is mutable |
84 | 86 | ||||
85 | invalid_contents.extend( | 87 | invalid_contents.extend( | ||
86 | template.format(keyword=current_keyword) | 88 | template.format(keyword=current_keyword) | ||
87 | for current_keyword in self.invalid_keywords | 89 | for current_keyword in self.invalid_keywords | ||
88 | for template in self.templates | 90 | for template in self.templates | ||
89 | ) | 91 | ) | ||
90 | 92 | ||||
91 | for content in invalid_contents: | 93 | for content in invalid_contents: | ||
92 | with self.subTest(content=content): | 94 | with self.subTest(content=content): | ||
93 | m = mock_open(read_data=content) | 95 | m = mock_open(read_data=content) | ||
94 | with patch("builtins.open", m): | 96 | with patch("builtins.open", m): | ||
95 | result = validate_recipe("dummy_path.txt") | 97 | result = validate_recipe("dummy_path.txt") | ||
96 | self.assertFalse(result, "Error") | 98 | self.assertFalse(result, "Error") | ||
97 | 99 | ||||
98 | def test_bad_recipe_file(self): | 100 | def test_bad_recipe_file(self): | ||
99 | error_cases = [OSError, IOError] | 101 | error_cases = [OSError, IOError] | ||
100 | 102 | ||||
101 | for error in error_cases: | 103 | for error in error_cases: | ||
102 | with self.subTest(error=error): | 104 | with self.subTest(error=error): | ||
103 | with patch("builtins.open", side_effect=error): | 105 | with patch("builtins.open", side_effect=error): | ||
104 | with self.assertRaises(RuinedNikuldenDinnerError): | 106 | with self.assertRaises(RuinedNikuldenDinnerError): | ||
105 | validate_recipe("missing_file.txt") | 107 | validate_recipe("missing_file.txt") | ||
106 | 108 | ||||
107 | if __name__ == "__main__": | 109 | if __name__ == "__main__": | ||
108 | unittest.main() | 110 | unittest.main() |
Legends | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
|
f | 1 | import unittest | f | 1 | import unittest |
2 | from unittest.mock import mock_open, patch | 2 | from unittest.mock import mock_open, patch | ||
3 | from secret import validate_recipe, RuinedNikuldenDinnerError | 3 | from secret import validate_recipe, RuinedNikuldenDinnerError | ||
4 | 4 | ||||
5 | def memoization(func): | 5 | def memoization(func): | ||
6 | data = {} | 6 | data = {} | ||
7 | 7 | ||||
8 | def wrapper(*args): | 8 | def wrapper(*args): | ||
9 | if args in data: | 9 | if args in data: | ||
10 | return data[args] | 10 | return data[args] | ||
11 | result = func(*args) | 11 | result = func(*args) | ||
12 | data[args] = result | 12 | data[args] = result | ||
13 | return result | 13 | return result | ||
14 | 14 | ||||
15 | return wrapper | 15 | return wrapper | ||
16 | 16 | ||||
17 | @memoization | 17 | @memoization | ||
18 | def generate_all_variations(word): | 18 | def generate_all_variations(word): | ||
19 | size = len(word) | 19 | size = len(word) | ||
20 | result = [] | 20 | result = [] | ||
21 | for i in range(1 << size): | 21 | for i in range(1 << size): | ||
22 | current = [] | 22 | current = [] | ||
23 | for j in range(size): | 23 | for j in range(size): | ||
24 | if i & (1 << j): | 24 | if i & (1 << j): | ||
25 | current.append(word[j].upper()) | 25 | current.append(word[j].upper()) | ||
26 | else: | 26 | else: | ||
27 | current.append(word[j].lower()) | 27 | current.append(word[j].lower()) | ||
28 | result.append(''.join(current)) | 28 | result.append(''.join(current)) | ||
29 | return result | 29 | return result | ||
30 | 30 | ||||
31 | @memoization | 31 | @memoization | ||
32 | def generate_all_insertions_for_keyword(word, special_word): | 32 | def generate_all_insertions_for_keyword(word, special_word): | ||
33 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | 33 | return [word[:i] + special_word + word[i:] for i in range(len(word) + 1)] | ||
34 | 34 | ||||
35 | @memoization | 35 | @memoization | ||
36 | def generate_all_insertions_for_random(word, special_word): | 36 | def generate_all_insertions_for_random(word, special_word): | ||
37 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | 37 | return [special_word[:i] + word + special_word[i:] for i in range(len(special_word) + 1)] | ||
38 | 38 | ||||
39 | 39 | ||||
40 | class TestNikuldenValidator(unittest.TestCase): | 40 | class TestNikuldenValidator(unittest.TestCase): | ||
41 | 41 | ||||
42 | def setUp(self): | 42 | def setUp(self): | ||
43 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | 43 | self.keywords = ["риба", "рибена", "шаран", "сьонга"] | ||
t | 44 | self.special_word = "рок" | t | 44 | self.special_word = "тр" |
45 | self.valid_keywords = set() | 45 | self.valid_keywords = set() | ||
46 | self.invalid_keywords = set() | 46 | self.invalid_keywords = set() | ||
47 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | 47 | self.some_edge_cases = ["", "\t", "\n", "123", "!", "?", ".", ","] | ||
48 | 48 | ||||
49 | self.templates = [ | 49 | self.templates = [ | ||
50 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | 50 | "Днес ще ям {keyword}, защото е Никулден ;Д.", | ||
51 | "Тази рецепта включва {keyword}.", | 51 | "Тази рецепта включва {keyword}.", | ||
52 | "{keyword} е подходящо ястие за Никулден.", | 52 | "{keyword} е подходящо ястие за Никулден.", | ||
53 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | 53 | "Нямам против да приготвя {keyword}, знаейки че иначе ще бъде жена ми ;(.", | ||
54 | "{keyword} е доста екзотично и полезно ястие." | 54 | "{keyword} е доста екзотично и полезно ястие." | ||
55 | "Практически {keyword} е клас {keyword} нали???" | 55 | "Практически {keyword} е клас {keyword} нали???" | ||
56 | ] | 56 | ] | ||
57 | 57 | ||||
58 | for current_keyword in self.keywords: | 58 | for current_keyword in self.keywords: | ||
59 | variations = generate_all_variations(current_keyword) | 59 | variations = generate_all_variations(current_keyword) | ||
60 | self.valid_keywords.update(variations) | 60 | self.valid_keywords.update(variations) | ||
61 | 61 | ||||
62 | def test_valid_recipe(self): | 62 | def test_valid_recipe(self): | ||
63 | valid_contents = [ | 63 | valid_contents = [ | ||
64 | template.format(keyword=current_keyword) | 64 | template.format(keyword=current_keyword) | ||
65 | for current_keyword in self.valid_keywords | 65 | for current_keyword in self.valid_keywords | ||
66 | for template in self.templates | 66 | for template in self.templates | ||
67 | ] | 67 | ] | ||
68 | 68 | ||||
69 | for content in valid_contents: | 69 | for content in valid_contents: | ||
70 | with self.subTest(content=content): | 70 | with self.subTest(content=content): | ||
71 | m = mock_open(read_data=content) | 71 | m = mock_open(read_data=content) | ||
72 | with patch("builtins.open", m): | 72 | with patch("builtins.open", m): | ||
73 | result = validate_recipe("dummy_path.txt") | 73 | result = validate_recipe("dummy_path.txt") | ||
74 | self.assertTrue(result, "Error") | 74 | self.assertTrue(result, "Error") | ||
75 | 75 | ||||
76 | def test_invalid_recipe(self): | 76 | def test_invalid_recipe(self): | ||
77 | for current_keyword in self.valid_keywords: | 77 | for current_keyword in self.valid_keywords: | ||
78 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | 78 | variations_keyword = generate_all_insertions_for_keyword(current_keyword, self.special_word) | ||
79 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | 79 | variations_random = generate_all_insertions_for_random(current_keyword, self.special_word) | ||
80 | self.invalid_keywords.update(variations_keyword) | 80 | self.invalid_keywords.update(variations_keyword) | ||
81 | self.invalid_keywords.update(variations_random) | 81 | self.invalid_keywords.update(variations_random) | ||
82 | 82 | ||||
83 | invalid_contents = self.some_edge_cases.copy() | 83 | invalid_contents = self.some_edge_cases.copy() | ||
84 | 84 | ||||
85 | invalid_contents.extend( | 85 | invalid_contents.extend( | ||
86 | template.format(keyword=current_keyword) | 86 | template.format(keyword=current_keyword) | ||
87 | for current_keyword in self.invalid_keywords | 87 | for current_keyword in self.invalid_keywords | ||
88 | for template in self.templates | 88 | for template in self.templates | ||
89 | ) | 89 | ) | ||
90 | 90 | ||||
91 | for content in invalid_contents: | 91 | for content in invalid_contents: | ||
92 | with self.subTest(content=content): | 92 | with self.subTest(content=content): | ||
93 | m = mock_open(read_data=content) | 93 | m = mock_open(read_data=content) | ||
94 | with patch("builtins.open", m): | 94 | with patch("builtins.open", m): | ||
95 | result = validate_recipe("dummy_path.txt") | 95 | result = validate_recipe("dummy_path.txt") | ||
96 | self.assertFalse(result, "Error") | 96 | self.assertFalse(result, "Error") | ||
97 | 97 | ||||
98 | def test_bad_recipe_file(self): | 98 | def test_bad_recipe_file(self): | ||
99 | error_cases = [OSError, IOError] | 99 | error_cases = [OSError, IOError] | ||
100 | 100 | ||||
101 | for error in error_cases: | 101 | for error in error_cases: | ||
102 | with self.subTest(error=error): | 102 | with self.subTest(error=error): | ||
103 | with patch("builtins.open", side_effect=error): | 103 | with patch("builtins.open", side_effect=error): | ||
104 | with self.assertRaises(RuinedNikuldenDinnerError): | 104 | with self.assertRaises(RuinedNikuldenDinnerError): | ||
105 | validate_recipe("missing_file.txt") | 105 | validate_recipe("missing_file.txt") | ||
106 | 106 | ||||
107 | if __name__ == "__main__": | 107 | if __name__ == "__main__": | ||
108 | unittest.main() | 108 | unittest.main() |
Legends | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
|