Diversion 2
Continuing Diversion. It’s just too long!
I’ll repeat the reflection from last time, and pick up from there.
Reflection
I got in more trouble than I wanted there. If I had committed once closer to when the trouble started, I might have felt better about rolling back. I think that GeePaw, who is much more organized than I am, would have rolled back, likely more than once. He does not like being out on a weak limb … nor do I, but I did what I did. My rules here are that I write up what I really do (as does GeePaw) and you get to see me making mistakes.
Just a bit more trouble, and I’d have had no choice but to roll back, or dig in for a very long debugging session. I know in my heart that rolling back and doing over is faster, so I hope I’d have done that. But I am aware that I might have just kept digging, and this session would be even longer than it already is.
I think we’ll move on now. There is an idea starting to form in my head: a NameKeeper. Right now we just have a list of known names. We know that somewhere near here, we will have a list of new names as well as known ones, and we could punch it in at this level. But I think we’ll do better with a little object.
NameKeeper will accept new names via, oh, add_name
, and it will produce two lists upon request: known_names
, and new_names
. Let’s TDD that little object, because I want to get back to working in my best style, not my worst.
NameKeeper
After hooking up new tests:
class TestNameKeeper:
def test_namekeeper(self):
nk = NameKeeper()
nk.add_name('T1')
assert nk.new_names() == ['T1']
nk.memorize()
assert nk.new_names() == []
assert nk.known_names() == ['T1']
That seems like a lot but I don’t think it really is:
class NameKeeper:
def __init__(self):
self._known_names = []
self._new_names = []
def add_name(self, name):
if name not in self._known_names:
if name not in self._new_names:
self._new_names.append(name)
def known_names(self):
return self._known_names
def new_names(self):
return self._new_names
def memorize(self):
self._known_names.extend(self._new_names)
self._new_names = []
My test is passing. Let’s write a harder one:
def test_phases(self):
nk = NameKeeper()
nk.add_name('T1')
nk.add_name('T2')
assert nk.known_names() == []
assert nk.new_names() == ['T1', 'T2']
nk.memorize()
assert nk.known_names() == ['T1', 'T2']
assert nk.new_names() == []
nk.add_name('T3')
nk.add_name('T1')
assert nk.known_names() == ['T1', 'T2']
assert nk.new_names() == ['T3']
nk.memorize()
assert nk.known_names() == ['T1', 'T2', 'T3']
assert nk.new_names() == []
That’s green too, no surprise. Commit: NameKeeper. Now let’s use the NameKeper in our Collator. Turn on the skipped test.
def test_two_harder_phases(self):
c = Collator()
c.add(TestStatus('T1', 'Pass'))
c.report()
c.add(TestStatus('T2', 'Pass'))
c.add(TestStatus('T1', 'Fail'))
result = c.report()
assert len(result) == 2
assert result[0].name == 'T1'
assert result[0].status == 'Fail'
assert result[1].name == 'T2'
assert result[1].status == 'Pass'
This is passing. I think I have been flummoxed by something. Anyway we need a failing test to plug in our NameKeeper, so let’s make this one even harder. But I’m tired of all this detailed testing.
def test_even_harder(self):
c = Collator()
c.add(TestStatus('T1', 'Pass'))
c.report()
c.add(TestStatus('T2', 'Pass'))
c.add(TestStatus('T1', 'Fail'))
previously_tested = c.report()
c.add(TestStatus('T3', 'Pass'))
c.add(TestStatus('T1', 'Fail'))
report_string = c.report_string()
assert report_string == 'T1F T2U T3P '
I propose to have a new method on my object, report_string(). In a moment, we’ll make the test harder but first let’s get this much.
class Collator:
def report_string(self):
string = ''
for result in self.report():
string += result.report_string() + ' '
return string
class TestResult:
def report_string(self):
return self.name + self.status[0]
Test is green. We still aren’t using our NameKeeper. Make the test harder, using our new method a lot.
def test_even_harder(self):
c = Collator()
c.add(TestStatus('T1', 'Pass'))
assert c.report_string() == 'T1Pnew '
c.add(TestStatus('T2', 'Pass'))
c.add(TestStatus('T1', 'Fail'))
assert c.report_string() == 'T1F T2Pnew '
c.add(TestStatus('T3', 'Pass'))
c.add(TestStatus('T1', 'Fail'))
report_string = c.report_string()
assert report_string == 'T1F T2U T3Pnew '
I’m pretty sure that’s what I want. Let’s make it so.
class Collator:
def __init__(self):
self.statuses = dict()
self.keeper = NameKeeper()
def add(self, status):
self.statuses[status.name] = status
self.keeper.add_name(status.name)
def report(self):
report = []
for name in self.keeper.known_names():
result = self.get_result(name)
report.append(result)
for name in self.keeper.new_names():
report.append(self.get_result(name))
self.keeper.memorize()
return report
def get_result(self, name):
try:
status = self.statuses[name]
result = TestResult(status.name, status.status)
except KeyError:
result = TestResult(name, "Unrun")
return result
I extracted a get_result
method, then added in a keeper instead of the old known_names
member, and used it. We create our report by reading both the old names and the new names. We have one more thing to do, however. Two tests are failing, let’s look at them.
def test_second_report_shows_unrun(self):
c = Collator()
c.add(TestStatus('T1', 'Pass'))
c.report()
unrun = c.report()
assert len(unrun) == 1
assert unrun[0].name == 'T1'
assert unrun[0].status == 'Unrun'
This is a defect. We need to clear the statuses when we have completed the report:
def report(self):
report = []
for name in self.keeper.known_names():
result = self.get_result(name)
report.append(result)
for name in self.keeper.new_names():
report.append(self.get_result(name))
self.keeper.memorize()
self.statuses.clear()
return report
Now just one failure:
def test_even_harder(self):
c = Collator()
c.add(TestStatus('T1', 'Pass'))
> assert c.report_string() == 'T1Pnew '
E AssertionError: assert 'T1P ' == 'T1Pnew '
No surprise there, we aren’t setting is_new yet. So we do:
It has to be done here.
for name in self.keeper.new_names():
report.append(self.get_result(name))
Let’s change the signature of get_result and do it there.
class Collator:
def report(self):
report = []
for name in self.keeper.known_names():
result = self.get_result(name, False)
report.append(result)
for name in self.keeper.new_names():
report.append(self.get_result(name, True))
self.keeper.memorize()
self.statuses.clear()
class TestResult;
def report_string(self):
return self.name + self.status[0] + ('new' if self.is_new else '')
We are green. Commit: working.
The code is not good. In fact, the code is nearly terrible. I’ve not done a really good job this morning, to the point where I wish I could scrap this article and do it over. But I owe it to you to show you what really happens, not what happens if I get seven tries.
Let’s improve the code, in the next article. and I’m going to split this one, I think. Yes.
Continued in Diversion 3