Предизвикателства > Валидатор за Никулденска рецепта


Валидатор за Никулденска рецепта
Краен срок: 06.12.2024 21:00
Точки: 1

### Прелюдия Тъй като наближава Никулден, решихме да ви развием риболовните и готварските умения... Разбира се, няма да направим нито едно от двете директно, но пък може да ви вдъхновим. Вместо ние да ви обясняваме за Никулденските традиции, ето ви цитат от интернеда: **На Никулден се яде риба, най-добре шаран. На този ден всеки трябва да си хапне риба.** **Народът казва:** “На Никулден човек трябва да си почопли зъбите с рибя кост, макар и от боклука да я вземе”. Няма да ви караме да ровите в боклука за рибени кости, но пък ще ви накараме да напишете малко тестове за нашия... ### Валидатор за Никулденска рецепта Искаме да напишем _(този път ние пишем, а вие тествате)_ валидатор за Никулденска рецепта - `validate_recipe`. Името можем да го сменим ако приложението стигне до пазара. Имаме следните изисквания за `validate_recipe`: - Приема име на файл. - Чете файла. - Проверява дали в него се съдържат някоя от следните думи - `['риба', 'рибена', 'шаран', 'сьонга']`. - Искаме да има `'рибена'`, ако случайно рецептата съдържа рибена глава. Пак е риба. - И да, `'сьонга'`, не _сьомга_. - Ако рецептата съдържа въпросната дума - функцията връща `True`, в противен случай - `False`. - Ако има проблем с четенето от файла - `OSError` или `IOError` - искаме да се хвърли `RuinedNikuldenDinnerError`, дефиниран в същия файл, както функцията. - Търсенето трябва да е case-insensitive, т.е. `'Риба'` е напълно валидно срещане на думата `'риба'`. Вие **няма** да пишете `validate_recipe`, това ще го напишем ние. Вашата задача е, да напишете тестове за въпросната функция. ### Тестовете Единствено с цел унификация ще наложим конкретни имена и изисквания за тестовете. Това всъщност е във ваша полза, а не ущърб, тъй като е директен hint какво би следвало да тествате. За да можете да тествате правилно, следва тестовете ви да започват със следното: ``` from secret import validate_recipe, RuinedNikuldenDinnerError ``` Горното **задължително** направете по този начин, а не с `import secret`, защото иначе тестовете, които тестват тестовете ви, ще фейлнат. ##### TestNikuldenValidator Тестовият клас, който очакваме от вас да дефинирате трябва да се казва `TestNikuldenValidator`, и да има следните 3 тестови метода: - `test_valid_recipe` - за случаите, в които рецептата е валидна. - `test_invalid_recipe` - за обратното на горното. - `test_bad_recipe_file` - за случаите, в които има проблем с четенето от файла. ### Уговорки - Смятаме, че се подразбира, но очакваме тестовете ви да са написани, използвайки unittest, а не някоя друга библиотека за автоматизирано тестване. - Да натъртим - **от вас очакваме в решенията ви да има САМО тестовете, а не реалната имплементация на `validate_recipe`**. А пък докато си ги пишете - правете каквото ви е кеф. Except drugs. Don't do drugs, kids. - Въпросите - в коментари, както винаги.
 1import io
 2import sys
 3import unittest
 4
 5
 6class StdBuffer:
 7    def __init__(self):
 8        self.buffer = io.StringIO()
 9
10    def __enter__(self):
11        self.stds = sys.stdin, sys.stderr, sys.stdout
12        sys.stdin = self.buffer
13        sys.stderr = self.buffer
14        sys.stdout = self.buffer
15
16    def __exit__(self, *args, **kwargs):
17        sys.stdin, sys.stderr, sys.stdout = self.stds
18
19
20class TestSanity(unittest.TestCase):
21    """Check if the test class is present."""
22
23    def setUp(cls):
24        # Importing the test dynamically to prevent including them
25        # in the scoring test suite
26        with StdBuffer():
27            cls.solution = __import__('solution')
28
29    def test_sanity(self):
30        self.assertIn('TestNikuldenValidator', dir(self.solution), 'Убеди се, че класът "TestNikuldenValidator" е наличен с точно това име.')
31        self.assertIn('test_valid_recipe', dir(self.solution.TestNikuldenValidator), 'Убеди се, че методът "test_valid_recipe" е наличен с точно това име.')
32        self.assertIn('test_invalid_recipe', dir(self.solution.TestNikuldenValidator), 'Убеди се, че методът "test_invalid_recipe" е наличен с точно това име.')
33        self.assertIn('test_bad_recipe_file', dir(self.solution.TestNikuldenValidator), 'Убеди се, че методът "test_bad_recipe_file" е наличен с точно това име.')
34
35
36if __name__ == "__main__":
37    suite = unittest.TestSuite()
38    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestSanity))
39    runner = unittest.TextTestRunner()
40    runner.run(suite)
  1import re
  2import unittest
  3from unittest.mock import patch
  4
  5import secret
  6from secret import FISH_OPTIONS, RuinedNikuldenDinnerError
  7
  8
  9def validate_recipe_no_lower(recipe_file_path):
 10    """Implementation missing lower."""
 11    try:
 12        with open(recipe_file_path) as recipe_file:
 13            recipe_text = recipe_file.read()
 14    except (IOError, OSError):
 15        raise RuinedNikuldenDinnerError
 16    return bool(re.search(fr'\b({"|".join(FISH_OPTIONS)})\b', recipe_text))
 17
 18def validate_recipe_no_error_handling(recipe_file_path):
 19    """Implementation missing error handling."""
 20    with open(recipe_file_path) as recipe_file:
 21        recipe_text = recipe_file.read()
 22    return bool(re.search(fr'\b({"|".join(FISH_OPTIONS)})\b', recipe_text, re.I))
 23
 24def validate_recipe_naive_in(recipe_file_path):
 25    """Implementation missing split when reading."""
 26    try:
 27        with open(recipe_file_path) as recipe_file:
 28            recipe_text = recipe_file.read().lower()
 29    except (IOError, OSError):
 30        raise RuinedNikuldenDinnerError
 31    return any(fish in recipe_text for fish in FISH_OPTIONS)
 32
 33
 34class TestTestNikuldenValidator(unittest.TestCase):
 35    """Test the tests for validate_recipe."""
 36
 37    def setUp(cls):
 38        # Importing the test dynamically to prevent including them
 39        # in the scoring test suite
 40        cls.TestNikuldenValidator = __import__('solution').TestNikuldenValidator
 41
 42    TEST_NAMES = {'test_valid_recipe', 'test_invalid_recipe', 'test_bad_recipe_file'}
 43
 44    def test_valid_validator(self):
 45        """Test with a valid implementation."""
 46        for test_name in self.TEST_NAMES:
 47            with patch('solution.validate_recipe',
 48                       side_effect=secret.validate_recipe) as mock_validate_recipe:
 49                result = self.TestNikuldenValidator(test_name)()
 50                self.assertTrue(result.wasSuccessful(),
 51                                f"Expecting {test_name} to pass with valid implementation.")
 52                mock_validate_recipe.assert_called()
 53
 54    def test_no_lower_validator(self):
 55        """Test with implementation missing lower."""
 56        expected_failure = 'test_valid_recipe'
 57        for test_name in self.TEST_NAMES:
 58            with patch('solution.validate_recipe',
 59                       side_effect=validate_recipe_no_lower) as mock_validate_recipe:
 60                result = self.TestNikuldenValidator(test_name)()
 61                if test_name == expected_failure:
 62                    self.assertFalse(result.wasSuccessful(),
 63                                     f"Expecting {test_name} to fail with an implementation missing lower.")
 64                else:
 65                    self.assertTrue(result.wasSuccessful(),
 66                                    f"Expecting {test_name} to pass with an implementation missing lower.")
 67                mock_validate_recipe.assert_called()
 68
 69    def test_no_error_handling_validator(self):
 70        """Test with implementation missing error handling."""
 71        expected_failure = 'test_bad_recipe_file'
 72        for test_name in self.TEST_NAMES:
 73            with patch('solution.validate_recipe',
 74                       side_effect=validate_recipe_no_error_handling) as mock_validate_recipe:
 75                result = self.TestNikuldenValidator(test_name)()
 76                if test_name == expected_failure:
 77                    self.assertFalse(result.wasSuccessful(),
 78                                     f"Expecting {test_name} to fail with an implementation with error handling.")
 79                else:
 80                    self.assertTrue(result.wasSuccessful(),
 81                                    f"Expecting {test_name} to pass with an implementation with error handling.")
 82                mock_validate_recipe.assert_called()
 83
 84    def test_naive_in_validator(self):
 85        """Test with implementation missing word splits."""
 86        expected_failure = 'test_invalid_recipe'
 87        for test_name in self.TEST_NAMES:
 88            with patch('solution.validate_recipe',
 89                       side_effect=validate_recipe_naive_in) as mock_validate_recipe:
 90                result = self.TestNikuldenValidator(test_name)()
 91                if test_name == expected_failure:
 92                    self.assertFalse(result.wasSuccessful(),
 93                                     f"Expecting {test_name} to fail with an implementation missing word splits.")
 94                else:
 95                    self.assertTrue(result.wasSuccessful(),
 96                                    f"Expecting {test_name} to pass with an implementation missing word splits.")
 97                mock_validate_recipe.assert_called()
 98
 99
100if __name__ == '__main__':
101    suite = unittest.TestSuite()
102    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestTestNikuldenValidator))
103    runner = unittest.TextTestRunner()
104    runner.run(suite)
Дискусия
Виктор Бечев
07.12.2024 12:41

Генерална бележка за решенията на предизвикателството, цитирано от PEP8: Surround **top-level** function and class definitions with **two** blank lines. _Method definitions inside a class_ are surrounded by a _single_ blank line. Extra blank lines may be used **(sparingly)** to separate groups of related functions. Blank lines may be omitted between a bunch of related one-liners (e.g. a set of dummy implementations). Use blank lines in functions, **sparingly**, to indicate logical sections. Най-честите "нарушения" са липсата на два реда за top-level дефиниции (това са импорти, константи, променливи, функции, класове, "хвърчащ" код като `if __name__ == ...`) и вече по-малко - прекалено либерална употреба на нови редове в тялото на функции / методи.
Георги Кунчев
06.12.2024 17:05

Само в отделна дума. Това не е валидно.
Павел Петков
06.12.2024 14:47

рибаааа валидно срещане ли е , защото текста съдържа риба или искаме риба да е отделна дума?
Йоан Байчев
05.12.2024 02:29

Нека разлгедаме примерното множеството {тор, Тор, тОр, тоР, ТОр, тОР, ТоР, ТОР}, то трябва ли да бъде асоцирано с думата "тор" спрямо case-insensitive или {тор, Тор} съответно? Относно съдържанието на файла, ние реално се интересуваме от това дали има някоя или няколко от "специалните думи" някъде във файла, тоест при наличие на нелогични думи, числа, специални символи и други, очакваното поведение на функцията би било да върне истина, ако "специална дума" бъде разпозната въпреки това? Примерът с "рибена глава", го разбирам, че функцията трябва да игнорира дума, която в себе си съдържа "специална дума" (префикс, инфикс и суфикс на нея).
Нишка
Виктор Бечев
05.12.2024 10:52

Ако търсехме за `"тор"` - всички от множеството са валидни, да. Не се опитваме да парснем файла за коректност, така че ако рецептата е "абвгд риба", важи. По отношение на третия ти въпрос - може би разбирам на къде биеш, но ми го перифразирай за да съм сигурен.
Виктор Бечев
05.12.2024 01:32

Един дисклеймър - досега не сме давали задача свързана с писане на тестове, така че го приемете като нещо експериментално. Да видим как ще се получи...