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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 
 | 
 | |||||||||