¿ªÔÆÌåÓý

ctrl + shift + ? for shortcuts
© 2025 Groups.io

Re: Changeset Evolution as an aid to Test Driven Development.


 


On Wed, Jun 16, 2021 at 10:52 AM Ron Jeffries <ronjeffriesacm@...> wrote:
I'm still trying to grok all this, despite having no earthly reason ever to do it ... I think I don't get how you use it as well as why. Will ask essentially the same question every time: how does this editing of the past help you accomplish this benefit listed?

On Jun 14, 2021, at 7:11 PM, John Carter via <john.carter@...> wrote:

The powerful growing observability and diffing logs debug technique is an excellent reason.

What can you observe and diff more easily this way than without.

The poster child environment for this trick is embedded / real time / multithreaded. The further you're away from that the less benefit it provides.

On an embedded target events are firing from the many different hardware subsystems at whatever time and in whatever order the hardware pleases and is handled in whatever order the scheduler pleases.

Defect free code for _a_ module will cope with that, and sequence it's behaviour into something sensible and orderly. ie. Logs for _all_ modules mixed together will be pretty chaotic.? For a well behaved module, usually pretty orderly.

The most difficult subtle horrible bugs in a multithreaded / real time environment arise from racy behaviour. ie. The code hasn't been written robust against different ordering or timing of events.

All else being the same, refactorings of all and every sort will alter the ordering and timing of events, which in defect free code will make no difference...

So if a refactoring, somewhere in your pile of refactorings, mixed up with a pile of behaviour changes, did something/triggered something nasty. Your pain is immeasurable.

If you had adequate logging you would at least see where the behaviour changed, but you haven't. So you add logging, as per tradition at the end of your branch, and yes, you can see something changed somewhere in your pile. You have no clue as to which, you have to start as if you? know nothing.

Which is where this technique becomes a super power.... go back to the start of the branch... add logging for the module of interest, record the real time behaviour.

Rebase pile of refactorings on top, record new real time behaviour.... if true refactoring, nothing interesting will have changed.

If something noxious and subtle is happening (perhaps even due to a preexisting bug), you can see exactly where the behaviour changed. Often that is enough to narrow down the exact changeset. Otherwise bisect to find it. Quite possibly that refactoring merely _exposed_ racy behaviour rather than introduced it.

In which case? extend unit test to catch it, (insert at end of unit test region of branch), fix bug, check to see it did and then decide where you want to drop the fix.

As I said, the poster child is embedded / real time / multithreaded, but value from this trick can even be gained within unit tests when the class under test is too complex to understand, and a refactoring broke something that the unit test coverage didn't catch.

It tells you where precisely coverage is needed.

But hey, why refactor if everything is easy and simple to understand? It's the gnarliest and worst modules with the most debt that most need refactoring.

Having strong proofs, that just keep getting stronger, that refactorings are indeed pure refactorings is an excellent reason.

How is it that the proofs get stronger? How is it different from putting the improved proofs at the tip of the spear? Are you building old versions just to run new tests on them?

If you are green fields and the code has never been released.. you get nothing.

If the code has man years of testing and man decades or centuries of use in the field.....? you need fairly strong evidence that you are making things better, rather than just changing things and maybe making it worse. So extending the tests _after_ behavioural changes loses that valuable oracle.?

Remember tests are code hence tests have bugs, tests test the code and the code tests the tests.

So expanding tests on known working code, and adding precondition asserts, provides a strong oracle to prove that the tests are correct and working.

Which as you grow the tests, they turn around and provide a strong oracle to prove your refactorings are correct and are indeed pure refactorings.

?

The "keeping up with the herd of cats" to avoid Bing Bang integrations is an excellent reason.

How does this help?

Breaks it down into small manageable integrations... and instead of finding later you are conflicting with weeks worth of work with another cat...

You find out after only a days worth of conflicts, know to walk over and work _with_ the other cat. Yup, shouldn't ever happen good team work and all that.... blah blah, but shit and schedule pressure happens.

Hanging out on a branch whilst man years is going into the mainline is going to be bad no matter what, but this trick decreases the urgency to close slightly.

Being able to drop the extended unit tests into the mainline to stop the herd of cats accidentally breaking things, even if the rest of the branch isn't ready, is an excellent reason.

How does this help? Wouldn't all your unit tests be available at the tip anyway?

Interposing behavioural changes with behavioural changes from other cats makes review and defect isolation hard. So if you're doing this mix of things, being able to drop the zero / low risk items first until you can build coverage and confidence. In the realm of excellent unit test coverage not so much of a problem, in browner fields, deeply embedded, multi-threaded, more of a problem.

Floods of microcommits of "obviously correct" tiny refactorings is an excellent reason.

How does this help do microcommits?

Review. Makes it much much easier. Whether by yourself, or reviewer or pair. Bundle twenty or so refactorings... and it becomes damn hard to reason about.

I have great confidence in myself in making correct refactorings.... so can blithely pile change upon change upon change.

Alas, history shows that confidence is woefully unfounded.


Using coverage analysis to see where you're starting to walk on shaky ground, and then retroactively? brace with unit tests _before_ all your changes is an excellent reason.

Again, are you running/testing old versions? If not, how is this different from adding tests wherever you are?

Your unit tests are what give you confidence bravely refactor, knowing you aren't causing regressions.

Sadly, looking at coverage analysis I know myself and all other cats are way overly optimistic about what unit tests do in fact test. Again, it's hard to assert negatives. It's easy to assert behaviour, but hard to assert the absence thereof. (Mutation testing sounds like a very good thing, anyone know of a good C/C++ mutation testing framework?)

If a branch you have just refactored is showing red in coverage analysis.... your test? shouldn't be giving _any_ confidence about the change. Doesn't mean you need to abandon the change, just means you have to go back and brace it first.




This Communication is Confidential. We only send and receive email on the
basis of the terms set out at

Join [email protected] to automatically receive all group messages.