Skip to content

Commit d910118

Browse files
committedAug 2, 2017
Add some test coverage to merge-business-hours.
1 parent 3867864 commit d910118

File tree

4 files changed

+220
-52
lines changed

4 files changed

+220
-52
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
/dist
88
/venv
99
machine.vim
10+
*.swp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from unittest import mock
2+
3+
import pytest
4+
5+
import vim_turing_machine.machines.merge_business_hours.merge_business_hours
6+
import vim_turing_machine.struct
7+
import vim_turing_machine.turing_machine
8+
from vim_turing_machine.constants import INITIAL_STATE
9+
from vim_turing_machine.constants import NO_FINAL_STATE
10+
from vim_turing_machine.constants import YES_FINAL_STATE
11+
from vim_turing_machine.machines.merge_business_hours.merge_business_hours import invert_bit
12+
from vim_turing_machine.machines.merge_business_hours.merge_business_hours import invert_direction
13+
from vim_turing_machine.machines.merge_business_hours.merge_business_hours import MergeBusinessHoursGenerator
14+
from vim_turing_machine.struct import BACKWARDS
15+
from vim_turing_machine.struct import FORWARDS
16+
from vim_turing_machine.turing_machine import TuringMachine
17+
18+
19+
@pytest.yield_fixture(autouse=True)
20+
def mock_blank_character():
21+
"""Change the blank character to be a space so that it's easier to write test cases."""
22+
with mock.patch.object(
23+
vim_turing_machine.turing_machine,
24+
'BLANK_CHARACTER',
25+
' ',
26+
):
27+
with mock.patch.object(
28+
vim_turing_machine.struct,
29+
'VALID_CHARACTERS',
30+
('0', '1', ' '),
31+
):
32+
with mock.patch.object(
33+
vim_turing_machine.machines.merge_business_hours.merge_business_hours,
34+
'BLANK_CHARACTER',
35+
' ',
36+
):
37+
with mock.patch.object(
38+
vim_turing_machine.machines.merge_business_hours.merge_business_hours,
39+
'VALID_CHARACTERS',
40+
('0', '1', ' '),
41+
):
42+
yield
43+
44+
45+
@pytest.fixture
46+
def merger():
47+
return MergeBusinessHoursGenerator(num_bits=3)
48+
49+
50+
def run_machine(transitions, tape, initial_position=0, assert_tape_not_changed=False):
51+
machine = TuringMachine(list(transitions), quiet=True)
52+
machine.run(tape[:], max_steps=10000, initial_cursor_position=initial_position)
53+
54+
if assert_tape_not_changed:
55+
assert_tape(machine, tape)
56+
57+
return machine
58+
59+
60+
def assert_cursor_at_end_of_output(machine):
61+
end = len(machine.tape) - 1
62+
while end > 0 and machine.tape[end] == ' ':
63+
end -= 1
64+
65+
assert machine.cursor_position == end
66+
67+
68+
def assert_tape(machine, expected_tape):
69+
# Ignore any blanks at the end
70+
assert expected_tape == ''.join(machine.tape).rstrip()
71+
72+
73+
def test_invert_bit():
74+
assert invert_bit('0') == '1'
75+
assert invert_bit('1') == '0'
76+
with pytest.raises(AssertionError):
77+
invert_bit('not_valid')
78+
79+
80+
def test_invert_direction():
81+
assert invert_direction(FORWARDS) == BACKWARDS
82+
assert invert_direction(BACKWARDS) == FORWARDS
83+
with pytest.raises(AssertionError):
84+
invert_direction('not_valid')
85+
86+
87+
def test_move_n_bits(merger):
88+
machine = run_machine(
89+
merger.move_n_bits(
90+
initial_state=INITIAL_STATE,
91+
direction=FORWARDS,
92+
final_state=YES_FINAL_STATE,
93+
num_bits=4,
94+
),
95+
tape='01010111',
96+
assert_tape_not_changed=True,
97+
)
98+
99+
assert machine.cursor_position == 3
100+
101+
102+
def test_move_to_blank_spaces(merger):
103+
machine = run_machine(
104+
merger.move_to_blank_spaces(
105+
initial_state=INITIAL_STATE,
106+
direction=FORWARDS,
107+
final_state=YES_FINAL_STATE,
108+
final_character=' ',
109+
final_direction=BACKWARDS,
110+
num_blanks=2,
111+
),
112+
tape='01 1111 10',
113+
assert_tape_not_changed=True,
114+
)
115+
116+
assert machine.cursor_position == 6 # End of the 1111
117+
118+
119+
def test_copy_bits_to_end_of_output(merger):
120+
machine = run_machine(
121+
merger.copy_bits_to_end_of_output(
122+
initial_state=INITIAL_STATE,
123+
num_bits=3,
124+
final_state=YES_FINAL_STATE,
125+
),
126+
tape='10111 01',
127+
)
128+
129+
assert_tape(machine, ' 11 01101')
130+
assert_cursor_at_end_of_output(machine)
131+
132+
133+
@pytest.mark.parametrize('tape, final_state', [
134+
('101 100110', NO_FINAL_STATE),
135+
('101 100100', YES_FINAL_STATE),
136+
('101 111100', YES_FINAL_STATE),
137+
])
138+
def test_compare_two_sequential_numbers(merger, tape, final_state):
139+
machine = run_machine(
140+
merger.compare_two_sequential_numbers(
141+
initial_state=INITIAL_STATE,
142+
greater_than_or_equal_to_state=YES_FINAL_STATE,
143+
less_than_state=NO_FINAL_STATE,
144+
),
145+
tape=tape,
146+
initial_position=len(tape) - 1,
147+
assert_tape_not_changed=True,
148+
)
149+
150+
assert_cursor_at_end_of_output(machine)
151+
assert machine.current_state == final_state
152+
153+
154+
def test_erase_number(merger):
155+
machine = run_machine(
156+
merger.erase_number(
157+
initial_state=INITIAL_STATE,
158+
final_state=YES_FINAL_STATE,
159+
),
160+
tape='100101110',
161+
initial_position=5, # end of 101
162+
)
163+
164+
assert machine.cursor_position == 2
165+
assert_tape(machine, '100 110')

‎vim_turing_machine/machines/merge_business_hours/merge_business_hours.py

+23-21
Original file line numberDiff line numberDiff line change
@@ -200,22 +200,6 @@ def copy_closing_hour_and_merge(self, initial_state, final_state):
200200

201201
return transitions
202202

203-
def invert_bit(self, bit_value):
204-
if bit_value == '0':
205-
return '1'
206-
elif bit_value == '1':
207-
return '0'
208-
else:
209-
raise AssertionError('Invalid bit {}'.format(bit_value))
210-
211-
def invert_direction(self, direction):
212-
if direction == BACKWARDS:
213-
return FORWARDS
214-
elif direction == FORWARDS:
215-
return BACKWARDS
216-
else:
217-
raise AssertionError('Invalid direction {}'.format(direction))
218-
219203
def noop_when_non_blank(self, state, direction):
220204
return (
221205
StateTransition(
@@ -478,7 +462,7 @@ def about_to_compare_bits_state(bit_index, bit_value):
478462
# If the numbers are not equal
479463
StateTransition(
480464
previous_state=about_to_compare_bits_state(bit_index, bit_value),
481-
previous_character=self.invert_bit(bit_value),
465+
previous_character=invert_bit(bit_value),
482466
next_state=(
483467
FOUND_GREATER_THAN_OR_EQUAL_TO_STATE
484468
if (
@@ -487,12 +471,12 @@ def about_to_compare_bits_state(bit_index, bit_value):
487471
)
488472
else FOUND_LESS_THAN_STATE
489473
),
490-
next_character=self.invert_bit(bit_value),
491-
tape_pointer_direction=self.invert_direction(direction),
474+
next_character=invert_bit(bit_value),
475+
tape_pointer_direction=invert_direction(direction),
492476
)
493477
)
494478

495-
direction = self.invert_direction(direction)
479+
direction = invert_direction(direction)
496480

497481
# After we've determined the answer, we need to move to the end of the output array
498482
transitions.extend(
@@ -668,6 +652,24 @@ def check_if_there_is_any_input_left(self, initial_state, final_state):
668652
return transitions
669653

670654

655+
def invert_bit(bit_value):
656+
if bit_value == '0':
657+
return '1'
658+
elif bit_value == '1':
659+
return '0'
660+
else:
661+
raise AssertionError('Invalid bit {}'.format(bit_value))
662+
663+
664+
def invert_direction(direction):
665+
if direction == BACKWARDS:
666+
return FORWARDS
667+
elif direction == FORWARDS:
668+
return BACKWARDS
669+
else: # pragma: no cover
670+
raise AssertionError('Invalid direction {}'.format(direction))
671+
672+
671673
if __name__ == '__main__':
672674
input_string = json.loads(sys.argv[1])
673675
num_bits = int(sys.argv[2])
@@ -678,4 +680,4 @@ def check_if_there_is_any_input_left(self, initial_state, final_state):
678680
merge_business_hours = TuringMachine(gen.merge_business_hours_transitions(), debug=True)
679681
merge_business_hours.run(initial_tape=initial_tape, max_steps=5000)
680682

681-
print(decode_hours(''.join(merge_business_hours.get_tape()), num_bits))
683+
print(decode_hours(''.join(merge_business_hours.tape), num_bits))

‎vim_turing_machine/turing_machine.py

+31-31
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class TooManyStepsException(Exception):
2525

2626
class TuringMachine(object):
2727

28-
def __init__(self, state_transitions, debug=False):
28+
def __init__(self, state_transitions, debug=False, quiet=False):
2929
validate_state_transitions(state_transitions)
3030

3131
self._state_transitions = state_transitions
@@ -34,65 +34,68 @@ def __init__(self, state_transitions, debug=False):
3434
for state in state_transitions
3535
}
3636
self._debug = debug
37+
self._quiet = quiet
3738
self.initialize_machine(tape=[])
3839

39-
def initialize_machine(self, tape):
40-
self._tape = list(tape)[:] # Copy the initial tape since we mutate it
41-
self._cursor_position = 0
42-
self._current_state = INITIAL_STATE
40+
def initialize_machine(self, tape, initial_cursor_position=0):
41+
self.tape = list(tape)[:] # Copy the initial tape since we mutate it
42+
self.cursor_position = initial_cursor_position
43+
self.current_state = INITIAL_STATE
4344
self._num_steps = 0
4445

4546
def get_state_transition(self):
4647
try:
4748
return self._state_transition_mapping[
48-
(self._current_state, self._tape[self._cursor_position])
49+
(self.current_state, self.tape[self.cursor_position])
4950
]
5051
except KeyError:
5152
raise MissingStateTransition(
52-
(self._current_state, self._tape[self._cursor_position])
53+
(self.current_state, self.tape[self.cursor_position])
5354
)
5455

5556
def step(self):
5657
"""This implements an infinitely long tape in the right direction, but
5758
will error if you go beyond position 0"""
5859
transition = self.get_state_transition()
5960

60-
self._tape[self._cursor_position] = transition.next_character
61+
self.tape[self.cursor_position] = transition.next_character
6162

62-
self._cursor_position += transition.tape_pointer_direction
63+
self.cursor_position += transition.tape_pointer_direction
6364

64-
if self._cursor_position < 0:
65+
if self.cursor_position < 0:
6566
raise NegativeTapePositionException
6667

6768
# Make sure we haven't run more than 1 past the end of the tape. This
6869
# should never happen since we append to the tape over time.
69-
assert self._cursor_position <= len(self._tape)
70+
assert self.cursor_position <= len(self.tape)
7071

71-
if self._cursor_position >= len(self._tape):
72+
if self.cursor_position >= len(self.tape):
7273
# Fake the infinite tape by adding a blank character under the
7374
# cursor.
74-
self._tape.append(BLANK_CHARACTER)
75+
self.tape.append(BLANK_CHARACTER)
7576

76-
self._current_state = transition.next_state
77+
self.current_state = transition.next_state
7778

78-
if self._current_state in FINAL_STATES:
79+
if self.current_state in FINAL_STATES:
7980
self.final_state()
8081
elif self._debug:
8182
self.print_tape()
8283

8384
def final_state(self):
84-
print('Program complete. Final state: {}'.format(self._current_state))
85-
print(
86-
'The program completed in {} steps using a machine with {} transitions'.format(
87-
self._num_steps,
88-
len(self._state_transitions)
85+
if not self._quiet:
86+
print('Program complete. Final state: {}'.format(self.current_state))
87+
print(
88+
'The program completed in {} steps using a machine with {} transitions'.format(
89+
self._num_steps,
90+
len(self._state_transitions)
91+
)
8992
)
90-
)
91-
self.print_tape()
93+
self.print_tape()
94+
9295
raise StopIteration
9396

94-
def run(self, initial_tape, max_steps=None):
95-
self.initialize_machine(initial_tape)
97+
def run(self, initial_tape, max_steps=None, initial_cursor_position=0):
98+
self.initialize_machine(initial_tape, initial_cursor_position=initial_cursor_position)
9699

97100
if self._debug:
98101
self.print_tape()
@@ -109,22 +112,19 @@ def run(self, initial_tape, max_steps=None):
109112

110113
def print_tape(self):
111114
tape = ''
112-
for i, character in enumerate(self._tape):
113-
if i == self._cursor_position:
115+
for i, character in enumerate(self.tape):
116+
if i == self.cursor_position:
114117
tape += '{}{}{}{}'.format(colored.bg('red'), colored.fg('white'), character, colored.attr('reset'))
115118
else:
116119
tape += character
117120

118-
if i != len(self._tape) - 1:
121+
if i != len(self.tape) - 1:
119122
tape += ' | '
120123

121124
print(tape)
122-
print('State: {}'.format(self._current_state))
125+
print('State: {}'.format(self.current_state))
123126
print() # Add empty line
124127

125-
def get_tape(self):
126-
return self._tape
127-
128128

129129
def validate_state_transitions(state_transitions):
130130
seen = defaultdict(list)

0 commit comments

Comments
 (0)
Please sign in to comment.