The only two things you need to know about software project management

  1. If someone asks when something will be done, they are asking for the date when they can use it.
  2. Every day, ask yourself what could happen to make it so the software won’t be ready to use by that date and do something about that.

The result of project management is that the software is ready to be used on the publicized date.

The details about how to figure out the date should be, what it will be, negotiating that difference, how to communicate it, how to change it, who “they” are, what “they can use it” means, changing that definition, what kinds of problems could happen, what to do about a problem, how to track all of this, etc is what project management work is, but in the end it just boils down to those two things.

How to introduce tests to a codebase without them

I wrote Unit Testing Old Code in January 2004, and I stand behind the recommendations. In short, use unit tests to accomplish what you are trying to do right now. Tests will help you refactor and optimize without breaking something, they will help you represent new learnings as you read code, and they are a good thing to write to reproduce a bug before you fix it. In all three cases, it’s a means to an end.

Later that year, Working Effectively with Legacy Code [affiliate link] came out with the definitive advice on how to add tests to a codebase without them—introducing the concept of seams. Generally, seams are a way to change what code does with external control. It might not be the way you’d design it from the start, but it’s a good way to migrate untestable code to being testable. I reread it recently and the techniques are still useful and easy to adapt to modern languages.

If I were to update my advice from 2004, I’d say to adopt diff-cover. It’s a script that filters down your code coverage reports to show the test coverage of the changes in your branch. This lets you try to hit high coverage numbers on each PR without necessarily having high coverage overall.

Other than that, if you have an untested codebase, I’d still say not to make it a project to add tests widely. It’s something I do it in my codebase even though its coverage is high. I want to keep it that way.

If you don’t have any tests, I’d get the infrastructure into place to run tests locally and in CI and a few to get a few tests going. Then, use tests to get your work done faster. I cover this in my book, Swimming in Tech Debt, which isn’t coming out until March, but I have a story of how I did this at Trello that was published in The Pragmatic Engineer.

Mastodon via Hashtags

I have been on Mastodon for a little over two years, and until a week or so ago, I didn’t really get it. Most of my social media usage and posting has been on LinkedIn, which I use because I know the people there in real life and care about what is happening to them. I post there because I also think they care about what’s going on with me. So, I have groomed my feed down to mostly those people. I thought I could replicate this on Mastodon, but that didn’t work.

The problem is that I know very few people on Mastodon in real life. I followed those that I could find, but I don’t keep looking for them, and Mastodon doesn’t suggest people I might know. I tried to augment this with hashtags, but I didn’t choose many and they happen to have very low traffic. So, my feed is dominated by a few people that I don’t know mostly posting about their lives and not on topics I care about.

But I recently decided to follow more higher traffic hashtags and that has made a big difference. It makes me think that Mastodon is somewhere between LinkedIn and Reddit. Reddit is about the topics. LinkedIn is about the people. Mastodon is something in-between.

Here’s how I use it now:

  1. I post to trial blog posts. This blog post was a toot a few days ago.
  2. I don’t link back here much. It might not be the best way to use social media, but I like the idea of content that’s fit for the media. My favorite toots are self-contained, so that’s what I mostly try to do. You can find this blog in my profile.
  3. I follow people that I see a few times. I figure they will mostly post on the topic.
  4. I follow people that star or boost me because I feel like they are probably also using Mastodon similar to how I use it.
  5. I mute accounts that post too much to hashtags. Mostly, these are businesses, not people.
  6. I have a few mute words to keep political news out of my feed.

This has made Mastodon more useful for me. I end up with a feed that is mostly about topics that I want to write about, and I post things I am thinking about. It’s supporting my aspirations to write more, and so I’ll keep using it this way for now.

If you want a code review to go fast, do it yourself

I am the only developer in my codebase right now, so I do all of my own code reviews. I post a PR, and then go review it. It goes quickly—I rarely have to wait.

But this is not what I’m talking about.

If you post a review, and the reviewer finds a lot of problems, that’s not because they are good reviewer. If QA finds obvious bugs, we know it’s because the work is bad. This is the same thing. Code reviewers and QA should not be finding obvious problems because there shouldn’t be any.

One reason this happens is that we feel pressured to get work done quickly, and so we post PRs that are not finished. The code can pass a simple review, but perhaps it doesn’t deal with edge cases or does things in a sloppy way with no unit-tests or is convoluted in a way that works, but is hard to read. Maybe the code is AI generated and PR’d after a cursory check.

The problem in each of these cases is not that AI generated code or that code was convoluted or that tests weren’t there. It’s that the author didn’t review their own code and fix these issues before opening the PR.

So, the author got their task done quickly, but now the code review eats up all of that saved time. It also obscures the problems that could have been found if the reviewer hadn’t gotten tired of making obvious comments. The author ends up spending more time, the reviewer is wasting all of their time, and all of this causes a big gap between calendar time and implementation time, which is a driver in incorrect estimates and low developer productivity.

The low productivity problem is insidious. It feels like we’re coding as fast as we can, but the unnecessary wait time builds up and makes projects late. Those wait times are rarely taken into account in an estimate—nor should they be. Instead of accounting for them, you should be eliminating them.

So, don’t post PRs of unfinished work that is going to cause the PR to be sent back (or worse, cause the ticket to be reopened). The best way to do this is to do your own code review first, fix all of the problems you find, fix up the commits, and then post the PR for a real review. By that time, the code should be correct and easy to see that it’s correct. That means:

  1. You ran all of the automated checkers/formatters already and they all pass (maybe even ones that are not in your CI).
  2. You tested your changes (see diff-cover to filter code coverage reports to your branch’s changes)
  3. You tested edge cases
  4. If you got to a solution with convoluted code (which is totally fine), you did a refactoring pass to make it easier to understand
  5. If you generated code with AI, you did your own editing pass on it to fix obvious problems (like distracting comments).
  6. You did an AI assisted code review of your own code and thought about each issue critically. Collect examples of your past reviews that got comments that should have been avoided and use them to prompt your AI reviews (to avoid repeating mistakes).
  7. The PR commits are created carefully to be easy to review one-at-a-time. So, whatever problems you found are not added on commits, but squashed to make it look like the code was written correctly to begin with. Don’t make a reviewer read code in one commit that is fixed or removed in a subsequent one.
  8. If you know that there are things that are unfinished in this PR, you explained that in your PR description and when they will be done.

Do this for your own enjoyment at work. When you find your own problems, it makes you feel good about yourself and boosts your self-esteem. When someone else points out your flaws, it doesn’t.

Use AI to Code Review Yourself, Not Others

In What to do about excessive (good) PR comments I wrote that a PR with a lot of good, actionable comments might be a sign that the author didn’t do enough beforehand to post an acceptable PR. It depends on many factors. For example, a new hire or junior developer might still need training, and so you would expect PRs to need some work,

But, there are some things that should never make it to review. We have linters and automatic code formatters, so we should never be correcting indentation in a review. If you use Rust, you are never finding a dangling pointer bug. There are automated tools for finding accessibility issues and security issues—which aren’t perfect, but they are probably better (and definitely faster) than most humans. All of these tools are somewhat mechanical, though, built on parser technology. We can do “better” with AI.

AI writes code good enough to be a starting point, and it can also review code. Since I think that the point of code reviews is information sharing, we wouldn’t want to replace them with AI. But AI is the perfect thing to use on yourself to make sure you don’t open PRs with obvious problems that slow down the review.

Mastodon Debugging

Last week I wrote a post on Mastodon about a problem I was having and then while I was describing it, I came up with a way to move forward:

Trying to figure out the right way to deploy a web and mobile web client on the same server. They are both SPA’s so mostly just need their packages to get served. Stymied by not having an exact local deployment.

Should solve that first actually. Thanks mastodon teddy bear.

The last sentence is a reference to “Teddy Bear Debugging’. The idea is that before you ask for help, you go over to an anthropomorphized inanimate object and explain the problem to it. If your description is detailed, you often just solve it yourself. This apparently also works with Mastodon toots. I solved my problem later that day by building a faithful local deployment.

Then, I realized that a mastodon is an animal that there might be a plush version of, and when I searched, I found this: https://shop.joinmastodon.org/products/mastodon-plushie — Mastodon (the organization) sells a plush mastodon! At the time, it was sold out and for Europe only, but someone tooted at me when they went on sale in the US a few days later. I felt compelled to order one to support Mastodon, but also because it’s a great debugging tool.

Why even do code reviews?

For most of my career, code reviews were rare. They weren’t directly supported by the most common source controls systems I used (RCS, SourceSafe, CVS, SVN, and early TFS) until git. Branching was hard to impossible, so we just all committed to the main branch. As a lead, I would read a lot of diffs after the fact, but not all of them, and I rarely sent comments.

I did, however, adopt unit tests and CI relatively early—in 2000, right after I read eXtreme Programming Explained [affiliate link]. So every change was unit tested soon after it was committed. We didn’t do code reviews, but we did a reasonable amount of pair programming, which had similar effects. Frankly, even after adopting code reviews for every PR a decade later (and pairing less), I can’t say that software I worked on had noticeably fewer bugs. Seems about the same.

What code reviews did do was make sure that more people than the author knew about the changes that were happening. Code reviews were far more efficient than pairing, and could be done asynchronously. I did code reviews nearly every day I worked on Trello, and I never worked on a team codebase I felt more generally comfortable with.

I think the defect-finding part of code reviews can be done by AI or other automated tools better than most humans—certainly faster. It’s not easy to see bugs just by reading code unless it’s egregious, and the automated tools are great at finding the egregious errors. Type systems and unit tests are also better than most programmers.

But, the information sharing part—that’s the real reason I think code reviews are important. I recently wrote that legacy code is code we didn’t write or review, so we shouldn’t let automation take over this part yet.

How we did CI in 2000

Right after I read eXtreme Programming: Explained [affiliate link], we built a CI system at Droplets (where I worked). CruiseControl didn’t exist yet, so anyone trying to do this just figured it out for themselves. Our system was based on a physical build token.

All of our desktop machines were named after movies—the CI Build Machine was named Goodfellas. I printed out an 5×7 poster of the movie and put the manual instructions for doing CI on the back. Then, I laminated it. You needed to have this card on your desk if you wanted to commit code.

Here is what it said on the back of the card:

  1. You must have this card on your desk to proceed
  2. Commit your code
  3. Go over to Goodfellas (don’t take the card)
  4. Get your changes
  5. Run the build and test scripts
  6. If it’s broken, fix it
  7. If you know someone was waiting for this card, give it to them
  8. Otherwise, put the card back where you found it

The build and test were automated, but you did need to manually grab the card.

Doing this solved our problem of broken builds caused by “it worked on my machine” problems. Seems antiquated now, but before easy branching and merging, this one-at-a-time verification caught a lot of problems early.

Great Works of Software (Poetry Edition)

Three years ago, I identified five pieces of software that I thought were “great works” that were written by one or a very few number of people. In rough chronological order, they are C/Unix, The ARM instruction set, TeX, VisiCalc, and the Web Browser/Server. I didn’t pick them based on what the software did, but they are all basically programming languages. They also are somewhat old (> 30 years), yet still exist or have a lineage of derivative projects (i.e. they were not lost).

Since then, I thought of two others. They are also programming languages of a sort and both are enabled by the existence of the web. Both are in heavy use, albeit mostly through derivatives. They are also both relatively tiny programs—more like poetry than prose.

The first is Markdown by John Gruber—you can download the original Perl at that link. Markdown is ubiquitous and finished, in the sense that it hasn’t been updated, and it doesn’t need updates.

Even the derivatives don’t need updates. In 2016 I wrote the Swift implementation of Trello Flavored Markdown by porting our web client’s JavaScript version (so it would match exactly). It was a little longer than Gruber’s original, but not by much. Once it was done, it only had one update that I know of.

The second software poem is the original Wiki software by Ward Cunningham. I don’t know if the original source is available, but I used this Perl implementation he published when I wanted a locally hosted version—it was my original second brain. I use Obsidian for this now, which (of course) is based on Markdown.

It’s funny that both Gruber and Cunningham chose Perl to write their poems. Perl syntax lends itself to writing actual poems—I have a vague memory of seeing some in Larry Wall’s Perl book. I would also note that both poems have minimal dependencies. I don’t think Markdown uses anything that isn’t in Perl. WikiWikiWeb uses Berkeley DB by default, but I trivially replaced that with simple files in my version (which I can’t find, but I do have the files, and they are just text).

They were also built in a time when it was common to build web server software in Perl (Markdown is 2004, WikiWikiWeb is 1995). I learned Perl in 1993 at my first job, which we used to replace all of our shell and awk scripts. I built production websites with Perl until 1999, but Perl was still my go-to language for scripting until 2013 when I switched to Python for web backends, scripting, and anything where Pandas would help. I’m sure Perl has equivalents for everything I do, but I never learned modern Perl.

My attempt at a poem of this nature is Page-o-Mat, a YAML-based language for making journals. It’s small, but I need YAML and PDF dependencies to make that happen. That fact about modern programming makes me think we won’t see another great software poem again.

How to Reduce Legacy code

There are two books that I read that try to define legacy code: Working Effectively with Legacy Code [affiliate link] by Michael Feathers and Software Design X-Rays [affiliate link] by Adam Tornhill. Briefly, Feathers says it’s code without tests and Tornhill adds that it’s code we didn’t write. In both cases, there’s an assumption that the code is also low quality.

To deal with the first problem, you should of course, add tests, and Feathers gives plenty of tips for how to do that. I would add that you should use a tool like diff-cover to make that happen more often. Diff-cover takes your unit test code coverage reports and filters them down so that you can see the code changed in the current branch that isn’t covered. Often, that code is inside of other functions, so covering your code will reduce the legacy code you touched.

This makes sense because once you have changed code, you really can’t say that you didn’t write it. Tornhill’s book describes software he sells that identifies legacy code using the repository history. It highlights code that no current engineer has touched. Your edit would remove that code from the list.

But, the author of the PR isn’t the only person who should be considered to know this code—the code reviewer should be counted too. If they have never seen this part of the codebase before, they should learn it enough to review the code. This is why I don’t like the idea of replacing human reviewers with AI. It has a role in reviews, but you can’t increase the number of developers that know a portion of a codebase by letting AI read it for you.

Every part of the codebase that is edited or reviewed by a new person reduces its “legacyness”. So does every test. If your gut tells you an area of the code is legacy, check the repo history and coverage. If you have a budget for dealing with engineering-led initiatives, assign a dev who hasn’t made edits to that area to write some tests.