From d59460256e9edeed40600d69ea15c7b02c2d8438 Mon Sep 17 00:00:00 2001 From: boryanagoncharenko Date: Tue, 1 Mar 2022 15:39:31 +0200 Subject: [PATCH] Add runtime validation and correction to the transpiled turtle commands #1983 (#2081) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- hedy.py | 35 +++++++++++++++++-------- tests/Tester.py | 38 ++++++++++++++++++++++++--- tests/test_level_01.py | 56 ++++++++++++++++++---------------------- tests/test_level_02.py | 46 ++++++++++++++++----------------- tests/test_level_03.py | 58 +++++++++++++++++++++--------------------- tests/test_level_04.py | 11 ++++---- tests/test_level_05.py | 10 +++----- tests/test_level_07.py | 10 +++----- 8 files changed, 148 insertions(+), 116 deletions(-) diff --git a/hedy.py b/hedy.py index 67ae1106252..d69b8688401 100644 --- a/hedy.py +++ b/hedy.py @@ -1211,13 +1211,8 @@ def comment(self, args): def forward(self, args): if len(args) == 0: - return self.make_forward(50) - - parameter = int(args[0]) - return self.make_forward(parameter) - - def make_forward(self, parameter): - return sleep_after(f"t.forward({parameter})", False) + return sleep_after('t.forward(50)', False) + return self.make_forward(int(args[0])) def turn(self, args): if len(args) == 0: @@ -1225,7 +1220,7 @@ def turn(self, args): arg = args[0] if self.is_variable(arg) or arg.lstrip("-").isnumeric(): - return f"t.right({arg})" + return self.make_turn(arg) elif arg == 'left': return "t.left(90)" elif arg == 'right': @@ -1235,6 +1230,24 @@ def turn(self, args): raise exceptions.InvalidArgumentTypeException(command=Command.turn, invalid_type='', invalid_argument=arg, allowed_types=get_allowed_types(Command.turn, self.level)) + def make_turn(self, parameter): + return self.make_turtle_command(parameter, Command.turn, 'right', False) + + def make_forward(self, parameter): + return self.make_turtle_command(parameter, Command.forward, 'forward', True) + + def make_turtle_command(self, parameter, command, command_text, add_sleep): + variable = self.get_fresh_var('trtl') + transpiled = textwrap.dedent(f"""\ + {variable} = {parameter} + try: + {variable} = int({variable}) + except ValueError: + raise Exception(f'While running your program the command {style_closest_command(command)} received the value {style_closest_command('{'+variable+'}')} which is not allowed. Try changing the value to a number.') + t.{command_text}(min(600, {variable}) if {variable} > 0 else max(-600, {variable}))""") + if add_sleep: + return sleep_after(transpiled, False) + return transpiled @@ -1287,7 +1300,7 @@ def ask(self, args): def forward(self, args): if len(args) == 0: - return self.make_forward(50) + return sleep_after('t.forward(50)', False) if ConvertToPython.is_int(args[0]): parameter = int(args[0]) @@ -1303,11 +1316,11 @@ def turn(self, args): arg = args[0] if arg.lstrip('-').isnumeric(): - return f"t.right({arg})" + return self.make_turn(arg) hashed_arg = hash_var(arg) if self.is_variable(hashed_arg): - return f"t.right({hashed_arg})" + return self.make_turn(hashed_arg) # the TypeValidator should protect against reaching this line: raise exceptions.InvalidArgumentTypeException(command=Command.turn, invalid_type='', invalid_argument=arg, diff --git a/tests/Tester.py b/tests/Tester.py index beea6e581ee..52ac36024f7 100644 --- a/tests/Tester.py +++ b/tests/Tester.py @@ -1,4 +1,4 @@ -import unittest +import textwrap import app import hedy, hedy_translation import re @@ -73,7 +73,7 @@ def as_list_of_tuples(*args): t = tuple((item[i] for item in args)) res.append(t) return res - + def multi_level_tester(self, code, max_level=hedy.HEDY_MAX_LEVEL, expected=None, exception=None, extra_check_function=None, expected_commands=None, lang='en', translate=True): # used to test the same code snippet over multiple levels # Use exception to check for an exception @@ -172,4 +172,36 @@ def validate_Python_code(parseresult): return True # programs with ask cannot be tested with output :( except Exception as E: return False - return True \ No newline at end of file + return True + + # The turtle commands get transpiled into big pieces of code that probably will change + # The followings methods abstract the specifics of the tranpilation and keep tests succinct + @staticmethod + def forward_transpiled(val): + return HedyTester.turtle_command_transpiled('forward', val) + + @staticmethod + def turn_transpiled(val): + return HedyTester.turtle_command_transpiled('right', val) + + @staticmethod + def turtle_command_transpiled(command, val): + command_text = 'turn' + suffix = '' + if command == 'forward': + command_text = 'forward' + suffix = '\n time.sleep(0.1)' + return textwrap.dedent(f"""\ + trtl = {val} + try: + trtl = int(trtl) + except ValueError: + raise Exception(f'While running your program the command {command_text} received the value {{trtl}} which is not allowed. Try changing the value to a number.') + t.{command}(min(600, trtl) if trtl > 0 else max(-600, trtl)){suffix}""") + + # Used to overcome indentation issues when the above code is inserted + # in test cases which use different indentation style (e.g. 2 or 4 spaces) + @staticmethod + def dedent(*args): + return '\n'.join([textwrap.indent(textwrap.dedent(a[0]), a[1]) if type(a) is tuple else textwrap.dedent(a) + for a in args]) diff --git a/tests/test_level_01.py b/tests/test_level_01.py index 987398db37f..0d93c796e77 100644 --- a/tests/test_level_01.py +++ b/tests/test_level_01.py @@ -73,7 +73,7 @@ def test_print_with_quotes(self): code=code, expected=expected, output="'Welcome to OceanView!'") - + def test_print_with_slashes(self): code = "print 'Welcome to \\O/ceanView!'" @@ -94,7 +94,7 @@ def test_print_with_slashed_at_end(self): expected=expected, output="Welcome to \\" ) - + def test_print_with_spaces(self): code = "print hallo!" expected = textwrap.dedent("""\ @@ -146,7 +146,7 @@ def test_ask_with_quotes(self): answer = input('\\'Welcome to OceanView?\\'')""") self.single_level_tester(code=code, expected=expected) - + def test_ask_nl_code_transpiled_in_nl(self): code = "vraag Heb je er zin in?" expected = "answer = input('Heb je er zin in?')" @@ -202,9 +202,7 @@ def test_echo_with_quotes(self): # forward tests def test_forward(self): code = "forward 50" - expected = textwrap.dedent("""\ - t.forward(50) - time.sleep(0.1)""") + expected = HedyTester.dedent(HedyTester.forward_transpiled(50)) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -214,9 +212,7 @@ def test_forward(self): def test_forward_arabic_numeral(self): code = "forward ١١١١١١١" - expected = textwrap.dedent("""\ - t.forward(1111111) - time.sleep(0.1)""") + expected = HedyTester.forward_transpiled(1111111) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -226,9 +222,7 @@ def test_forward_arabic_numeral(self): def test_forward_hindi_numeral(self): code = "forward ५५५" - expected = textwrap.dedent("""\ - t.forward(555) - time.sleep(0.1)""") + expected = HedyTester.forward_transpiled(555) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -237,10 +231,10 @@ def test_forward_hindi_numeral(self): ) def test_forward_without_argument(self): - code = textwrap.dedent("""forward""") + code = 'forward' expected = textwrap.dedent("""\ - t.forward(50) - time.sleep(0.1)""") + t.forward(50) + time.sleep(0.1)""") self.multi_level_tester( max_level=self.max_turtle_level, @@ -292,10 +286,9 @@ def test_one_turn_left(self): self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) - def test_turn_number(self): code = "turn 180" - expected = "t.right(180)" + expected = HedyTester.turn_transpiled(180) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -305,7 +298,7 @@ def test_turn_number(self): def test_turn_negative_number(self): code = "turn -180" - expected = "t.right(-180)" + expected = HedyTester.turn_transpiled(-180) self.multi_level_tester( max_level=10, code=code, @@ -332,28 +325,29 @@ def test_comment(self): # combined keywords tests def test_print_ask_echo(self): - code = textwrap.dedent("""\ + code = textwrap.dedent("""\ print Hallo ask Wat is je lievelingskleur echo je lievelingskleur is""") - expected = textwrap.dedent("""\ + expected = textwrap.dedent("""\ print('Hallo') answer = input('Wat is je lievelingskleur') print('je lievelingskleur is '+answer)""") - self.single_level_tester( - code=code, - expected=expected, - expected_commands=['print', 'ask', 'echo']) + self.single_level_tester( + code=code, + expected=expected, + expected_commands=['print', 'ask', 'echo']) def test_forward_turn_combined(self): - code = "forward 50\nturn\nforward 100" - expected = textwrap.dedent("""\ - t.forward(50) - time.sleep(0.1) - t.right(90) - t.forward(100) - time.sleep(0.1)""") + code = textwrap.dedent("""\ + forward 50 + turn + forward 100""") + expected = HedyTester.dedent( + HedyTester.forward_transpiled(50), + 't.right(90)', + HedyTester.forward_transpiled(100)) self.multi_level_tester( max_level=7, code=code, diff --git a/tests/test_level_02.py b/tests/test_level_02.py index 14f5461dcf4..efcbf5f4057 100644 --- a/tests/test_level_02.py +++ b/tests/test_level_02.py @@ -178,9 +178,9 @@ def test_turn_with_number_var(self): code = textwrap.dedent("""\ direction is 70 turn direction""") - expected = textwrap.dedent("""\ - direction = '70' - t.right(direction)""") + expected = HedyTester.dedent( + "direction = '70'", + HedyTester.turn_transpiled('direction')) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -202,9 +202,9 @@ def test_turn_with_non_ascii_var(self): code = textwrap.dedent("""\ ángulo is 90 turn ángulo""") - expected = textwrap.dedent("""\ - vefd88f42b64136f16e8f305dd375a921 = '90' - t.right(vefd88f42b64136f16e8f305dd375a921)""") + expected = HedyTester.dedent( + "vefd88f42b64136f16e8f305dd375a921 = '90'", + HedyTester.turn_transpiled('vefd88f42b64136f16e8f305dd375a921')) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -235,10 +235,9 @@ def test_forward_with_integer_variable(self): code = textwrap.dedent("""\ a is 50 forward a""") - expected = textwrap.dedent("""\ - a = '50' - t.forward(a) - time.sleep(0.1)""") + expected = HedyTester.dedent( + "a = '50'", + HedyTester.forward_transpiled('a')) self.multi_level_tester( max_level=self.max_turtle_level, code=code, @@ -282,28 +281,27 @@ def test_assign_print(self): print(f'{naam}')""") self.single_level_tester(code=code, expected=expected) + def test_forward_ask(self): code = textwrap.dedent("""\ - afstand is ask hoe ver dan? - forward afstand""") + afstand is ask hoe ver dan? + forward afstand""") - expected = textwrap.dedent("""\ - afstand = input('hoe ver dan'+'?') - t.forward(afstand) - time.sleep(0.1)""") + expected = HedyTester.dedent( + "afstand = input('hoe ver dan'+'?')", + HedyTester.forward_transpiled('afstand')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) - def test_turn_ask(self): code = textwrap.dedent("""\ - print Turtle race - direction is ask Where to turn? - turn direction""") + print Turtle race + direction is ask Where to turn? + turn direction""") - expected = textwrap.dedent("""\ - print(f'Turtle race') - direction = input('Where to turn'+'?') - t.right(direction)""") + expected = HedyTester.dedent("""\ + print(f'Turtle race') + direction = input('Where to turn'+'?')""", + HedyTester.turn_transpiled('direction')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) def test_assign_print_punctuation(self): diff --git a/tests/test_level_03.py b/tests/test_level_03.py index c7a52e0d854..1df9d58e379 100644 --- a/tests/test_level_03.py +++ b/tests/test_level_03.py @@ -292,12 +292,12 @@ def test_list_access_with_type_input_gives_error(self): def test_turn_number(self): code = textwrap.dedent("""\ - print Turtle race - turn 90""") + print Turtle race + turn 90""") - expected = textwrap.dedent("""\ - print(f'Turtle race') - t.right(90)""") + expected = HedyTester.dedent( + "print(f'Turtle race')", + HedyTester.turn_transpiled(90)) self.single_level_tester( code=code, @@ -311,10 +311,10 @@ def test_turn_number_var(self): direction is 70 turn direction""") - expected = textwrap.dedent("""\ - print(f'Turtle race') - direction = '70' - t.right(direction)""") + expected = HedyTester.dedent("""\ + print(f'Turtle race') + direction = '70'""", + HedyTester.turn_transpiled('direction')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) @@ -366,39 +366,39 @@ def test_assign_print(self): print(f'{naam}')""") self.single_level_tester(code=code, expected=expected) + def test_forward_ask(self): code = textwrap.dedent("""\ - afstand is ask hoe ver dan? - forward afstand""") + afstand is ask hoe ver dan? + forward afstand""") - expected = textwrap.dedent("""\ - afstand = input('hoe ver dan'+'?') - t.forward(afstand) - time.sleep(0.1)""") + expected = HedyTester.dedent( + "afstand = input('hoe ver dan'+'?')", + HedyTester.forward_transpiled('afstand')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) def test_turn_ask(self): code = textwrap.dedent("""\ - print Turtle race - direction is ask Where to turn? - turn direction""") + print Turtle race + direction is ask Where to turn? + turn direction""") - expected = textwrap.dedent("""\ - print(f'Turtle race') - direction = input('Where to turn'+'?') - t.right(direction)""") + expected = HedyTester.dedent("""\ + print(f'Turtle race') + direction = input('Where to turn'+'?')""", + HedyTester.turn_transpiled('direction')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) def test_random_turn(self): code = textwrap.dedent("""\ - print Turtle race - directions is 10, 100, 360 - turn directions at random""") + print Turtle race + directions is 10, 100, 360 + turn directions at random""") - expected = textwrap.dedent("""\ - print(f'Turtle race') - directions = ['10', '100', '360'] - t.right(random.choice(directions))""") + expected = HedyTester.dedent("""\ + print(f'Turtle race') + directions = ['10', '100', '360']""", + HedyTester.turn_transpiled('random.choice(directions)')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle()) diff --git a/tests/test_level_04.py b/tests/test_level_04.py index 15ea12d471a..1ff6b65b86b 100644 --- a/tests/test_level_04.py +++ b/tests/test_level_04.py @@ -337,12 +337,11 @@ def test_ask_assign(self): def test_forward_ask(self): code = textwrap.dedent("""\ - afstand is ask 'hoe ver dan?' - forward afstand""") - expected = textwrap.dedent("""\ - afstand = input(f'hoe ver dan?') - t.forward(afstand) - time.sleep(0.1)""") + afstand is ask 'hoe ver dan?' + forward afstand""") + expected = HedyTester.dedent( + "afstand = input(f'hoe ver dan?')", + HedyTester.forward_transpiled('afstand')) self.multi_level_tester( max_level=self.max_turtle_level, code=code, diff --git a/tests/test_level_05.py b/tests/test_level_05.py index dffad7041c0..e34a36672e8 100644 --- a/tests/test_level_05.py +++ b/tests/test_level_05.py @@ -93,12 +93,10 @@ def test_identifies_backtick_inside_conditional(self): # combined tests def test_turn_forward(self): result = hedy.transpile("forward 50\nturn\nforward 100", self.level) - expected = textwrap.dedent("""\ - t.forward(50) - time.sleep(0.1) - t.right(90) - t.forward(100) - time.sleep(0.1)""") + expected = HedyTester.dedent( + HedyTester.forward_transpiled(50), + "t.right(90)", + HedyTester.forward_transpiled(100)) self.assertEqual(expected, result.code) self.assertEqual(True, result.has_turtle) def test_ask_print(self): diff --git a/tests/test_level_07.py b/tests/test_level_07.py index 788e97c6085..6b8509f3948 100644 --- a/tests/test_level_07.py +++ b/tests/test_level_07.py @@ -8,13 +8,11 @@ class TestsLevel7(HedyTester): #repeat tests def test_repeat_turtle(self): - code = textwrap.dedent("""\ - repeat 3 times forward 100""") + code = "repeat 3 times forward 100" - expected = textwrap.dedent("""\ - for i in range(int('3')): - t.forward(100) - time.sleep(0.1)""") + expected = HedyTester.dedent( + "for i in range(int('3')):", + (HedyTester.forward_transpiled(100), ' ')) self.single_level_tester(code=code, expected=expected, extra_check_function=self.is_turtle())