test && commit || revert (TCR) was introduced by Kent Beck some weeks ago. Every time you run a test, your code gets either committed or deleted. This has a profound impact on how you develop software and what becomes possible.

Deleting code on every failed test? This sound unintuitive and it is. But it was the same with TDD (just think about the three absurd rules of TDD). After digging a bit deeper, you will gain more understanding of how to write code anyway. This post takes you through the journey, I took with our Meetup-Group.

tl;dr TCR has great potential after you got used to it. It contains two sperate ideas: test && commit for collaborating in teams and test || reset as “alternative” to TDD. We did an example and it worked fine.

How low can you go?

“Limbo lower now; Limbo lower now; How low can you go?” is the spoken refrain of Chubby Checkers “Libo rock.” Kent Beck uses this phrase for asking, how tight you can shorten the integration-cycle in a software project to scale the teams massively, for example to 100.000 Programmers working at one project.

Integrate faster!

We saw the first wave of integration-time shorting with the rise of Continous Integration which preached us to integrate daily or at least weekly to write better software. Daily is not “low” enough, and Limbo asks, “How low can you go?”

Ideal: Google Docs

My first thought to how low integration time can be is Google Docs. It allows writing with many different persons simultaneously in the same document while seeing each other’s cursor. In Google Docs we resolve conflicts as soon as they appear (most of them automatically), and we “check-in” (in a Git-Metaphor) in real-time while typing. If you zoom out, you can imagine a whole team or even an entire company, that is working on a complete folder-tree of Google-Docs saved in Google Drive. Each person has permissions per folder and therefore per subtree, and everyone can freely create, change and delete documents while these actions are propagated immediately to their colleagues. No blocking, no delay.

So the question “How low can you go?” is answered for via Google Docs (and Google Drive). So, why don’t we put all our source-code in a particular form of Google Docs?

Note: maybe we will — the idea sounds great; we see the first steps with Visual Studios Remote capabilities.

Source Code != Documents

The Problem with Google Docs: We don’t write the documents in plain English. The code is much more sensitive than plain English: Either it is syntactically incorrect (it does not build), or it is semantically incorrect (the tests do not pass). Moreover, our code has to be globally correct and not only in one file, because we use all source-files (simplified!) the build and test process. Therefore the second principle of Limbo (see below) is hurt with the Google Docs solution: “No one is allowed to cause others (including users) problems.” Actually, we are most of the time in an incorrect state, for example, while we are typing a variable name.

AST Transformations

How to resolve this? The trick is to stay consistently in a green state. If we achieve this, we can always merge without causing problems for others. Let’s look at the syntactical problems first. What happens if our program does not compile? It means that our compiler can’t convert our source-code into an abstract syntax tree (AST). An easy solution is, therefore, to manipulate the AST directly with a projectional editor. This editor allows only valid transformations of the syntax-tree. You can think of projectional editing as a set of changes of the AST.

MPS from JetBrains allows creating DSLs (Domain Specific Languages) which do this. Projectional editing leads to problems with current programming languages: The designers did not create these for this type of programming.

A Git Prototype

There is a more natural solution for us programmers: We only synchronize if we are in a green state (program compile and tests pass). This type of synchronization works with current languages and with Git smoothly. To do so, we just extend our test command:

test && commit

This command commits the code as soon our tests pass, which means that we compiled successfully as well; therefore we are green — syntactically and semantically.

Limbo Principles

Great! We have `test && commit` and therefore the first part of TCR. Let’s revisit the two Limbo principles as suggested by Kent Beck:

  1. Everyone is working on (and production is executing) the same program, represented by a single abstract syntax tree.
  2. No one is allowed to cause others (including users) problems.

We kind of solved the second problem. We are not synchronizing yet (we commit, but do not push) and even if we did, we would only propagate green code: No problems for others.

From Limbo to a Git Workflow

It is time to solve the first principle. If we worked in Google Docs, this would have been solved for us already. However, we are working with Git. Therefore, we can run this script (from Kent Beck):

while(true);
do
git pull -- rebase;
git push;
done;

As soon as we do our `commit && test`, the code gets pushed. As soon as someone else pushes, the code gets merged (just like in Google Docs except that we manually have to commit). We have everything for synchronization and to archive the first of the two principles.

Kent Beck suggests more than the solution to Limbo, we have seen so far:

test && commit || revert

The prototype for a collaboration tool is working (demo below)! However, is only half of TCR, which consists of two parts:

test && commit
test || revert

The first part is for collaboration in the Limbo style (together with the push/pull loop). The second leads to a programming methodology like TDD (test driven development). To clarify the `&&` and `||` look at this behavior:

$ echo “exit 0” > test
$ ./test && echo “commit|| echo “revert
commit
$ echo “exit 1” > test
$ ./test && echo “commit|| echo “revert
revert

Test-Frameworks result in the exit-code 0 if the tests pass and something greater `0`, if not. Which means:

if(test().result == passed) 
commit() // with the sync through the script
else
revert() // to latest green state; via git reset

Change the Act of Programming

TCR changes the way we create software dramatically (as TDD did). Every time we fail a test, we are in a red state and the ‘revert’ kicks in and reverts us to the last green state. It is like playing a computer game. Every time we die, we wake up the last checkpoint, where we are save of the monsters.

The ‘revert’ leads to very short iterations, because if we “invest” too much in a code at once, it becomes likely, that it gets deleted. With every new line, we introduce a new feature, that could fail. Moreover, our lost investment increases with every line. As we see later, this forces us to write minimal tests and only as much code as is necessary. This minimalism is good.

How TCR is it similar to TDD?

Let’s revisit Bob Martins three laws of TDD:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test that is sufficient to fail; compilation failures are failures.
  3. You are not allowed to write any more production code that is sufficient to pass the one failing unit-test.

TCR supports point three. If you write more production code than necessary, you produce an unneeded risk of deletion. You don’t even need discipline anymore to follow this rule, because you follow it naturally after you got used to TCR.

TCR supports the second point as well because otherwise you are forced to write much of the production code at once. While writing, you are not able to test it, because this would reset your written code. Therefore the second point becomes natural as well.

The first point still requires discipline. You start in a green state (no test). As soon you write code without tests, your semantic bugs are not detected by ‘test,’ and therefore the construct of TCR fails. A possible solution would be to add a coverage tool to the test, that fails if the coverage is not 100% (yes, I know the discussion of 100%!).

I have to conclude, that TCR follows/supports the three laws of TDD. However, it is not TDD!

How TCR differs from TDD?

I am still not sure if TDD and TCR are not orthogonal (I am happy, to hear arguments for both sides). However, revisiting the process of both. Let’s start with TDD:

  1. We begin in Green (do: `test`)
  2. We create a failing Unit-Test and are in red (do: `test`)
  3. We fix the test to be Green again (do: `test` — as often as necessary to arrive in Green)
  4. We refactor. We are in green and stay in green (do: `test`)

The significant difference is in the third step. As soon we run the tests, and we fail, your current attempt gets reset.

Note to me: this reminds me somehow to functional programming, where we have a stateless transformation. With TDD, we have a state, because the current attempt doesn’t get reset (To-Do: Explore this in another post).

For TCR, we need a specialization of TDD. Where we never come to the Red state or are at least as short as possible:

  1. We start Green (do: `test && commit || reset`)
  2. Write a test (do: `test && commit || reset src`)
  3. Fake the implementation (do: `test && commit || reset src`)
  4. Refactor, where you replace n Fakes with a real implementation (do: `test && commit || reset src`)

Every step involves just one or a few lines of code. As you probably recognized, I’ve written `test && commit || reset src` with `src`. We see the Reason later in the example section.

So the main difference is, that we feel with TDD very comfortable in the Red zone because it allows us to write production code. Therefore we can be quite a while in this phase and execute the tests often while we are there. We are in the Red state. On the other hand, in TCR, we try never to get in the Red state. If we recognize, we are there, ‘revert’ kicks in and brings us to the green state. Therefore you could see TCR as a ‘stateless’ (‘red-stateless’) version of TDD.

Stop Talking! Show me something.

As mentioned before, TCR consists of two parts:

  1. `test && commit` together with push/pull for collaboration
  2. `test || revert` as programming methodology (yea, I know, you need your commit here as well, but it no essential part)

To try the first part, you need a project with multiple parts/files and programmers, who are work simultaneously. I describe it in a later blog post in more detail. I concentrate here on the second aspect; for this is a straightforward program like ‘Fibonacci’ enough.

I start with a simple Java/JUnit/Gradle project, just because, it is very convenient for me. There is nothing special with the tooling. I am sure you can translate it into your favorite language and framework. The only requirement is that the tests return the code `0` on success and something other than `0` for failure — I know now test-framework, which does not provide this.

Here the init structure:

.
├── # gradle stuff
└── src
├── main
│ ├── java
│ │ └── Fib.java
└── test
├── java
│ └── TestFib.java

Also, prepare Git:

$ git init && git add . && git commit -m “init”

We can run our test with ‘./gradlew test’, so our ‘test && commit || reset’ is:

$ ./gradlew test && git commit -am working || git reset -- hard

what we call test, that it becomes natural to execute it:

$ echo “./gradlew test && git commit -am working || git reset — hard” > test
$ chmod +x test
$ ./test

We make sure that we have an execution environment:

public class TestFib {
@Test
public void nothing() { }
}
$ ./test

Great! We are done with our first TCR.

Problem: Not compiling

As we begin to code an uncertainty arises in me. Am I allowed to execute `./test`? As soon my program does not compile TCR reverts it. This kind of reverting the code is annoying, because it is fine, that my test is not compiling, because I specify my interface in the test first. Remember the second rule of TDD: “ You are not allowed to write any more of a unit test that is sufficient to fail; compilation failures are failures.”?

Therefore I extend my TCR-command in the ‘test’ file with:

$ ./gradlew build -x test && (./gradlew test && git commit -am working || git reset — hard)$ echo “./gradlew build -x test && (./gradlew test && git commit -am working || git reset — hard)” > test

I add the condition that TCR proceeds if my program compiles. It still not commits, so all requirements for Limbo are met.

Note: `-x test` is necessary, because grade would run the test as part of the build process.

I can now specify the interface of my function:

@Test
public void fib1_1() {
assertEquals(1, Fib.fib(1));
}

Sure enough, I get an error (`fib` is not defined), but it does not get reset. The next step is to fake the behavior so that I stay green:

public class Fib {
public static int fib(int n) {
return 1;
}
}
$ ./test

I am green. TCR reverted nothing. Let’s start with the second test case:

@Test
public void fib2_1() {
assertEquals(1, Fib.fib(2));
}
$ ./test

Still green.

@Test
public void fib3_2() {
assertEquals(2, Fib.fib(3));
}
$ ./test

This test failed, and TCR deleted it. First surprise. Let’s add it again together with the faked behavior:

@Test
public void fib3_2() {
assertEquals(2, Fib.fib(3));
}
- - - - - - - - - - - - - - -
public static int fib(int n) {
if(n == 3)
return 2;

return 1;
}
$ ./test

This method worked, and Git persisted it together with the test. Let’s take a little bit bigger step. `fib(7) = 13`

@Test
public void fib7_13() {
assertEquals(13, Fib.fib(7));
}

public static int fib(int n) {
if(n == 3)
return 2;
if(n == 7)
return 13;

return 1;
}
$ ./test

This code works again. Time for a refactoring:

public static int fib(int n) {
if(n == 1)
return 1;
return (n == 3) ? 2 : 13;

}
$ ./test

Error! `fib(2)` fails. TCR reverted my code! Try again:

public static int fib(int n) {
if(n <= 2)
return 1;
return (n == 3) ? 2 : 13;

}
$ ./test

Mini-Step: done! We still have duplication to remove: The `2`, `3` and `13` appear in the test and the code. Let’s remove them.

public static int fib(int n) {
if(n <= 2)
return 1;
return fib(n-1) — fib(n-2);
}

As my tests just told me, I was wrong; TCR reverted my code to a green state. Everything is working again. That’s good. Let’s try again:

public static int fib(int n) {
if(n <= 2)
return 1;
return fib(n-1) + fib(n-2);
}

That’s it. I’ve done Fibonacci using TCR with the concrete command:

./gradlew build -x test && (./gradlew test && git commit -am working || git reset -hard)

Note: Maybe I should call it BTCR for `build && (test && || revert)`.

Wait! What happens, if I pass negative integers? Let’s try it via an experiment as a test (as we would do it with TDD):

@Test(expected = IllegalArgumentException.class)
public void fibNegative_illegal() {
Fib.fib(-1);
}
$ ./test

Aaarggg. TCR deleted my test/experiment! I am just glad, that I have already copied it in this post. Such kind of reverting is annoying — TCR should not revert our tests. On the one hand side, we should be able to add tests without understanding the code (without the fear, that it gets deleted all the time) on the other hand TCR should still revert our production code.

Git should just revert the `src` folder, but not the `test` folder. `git reset` is not capable of doing this. Therefore, we have to replace it:

$ git checkout HEAD — src/main/
$ ./gradlew build -x test && (./gradlew test && git commit -am working || git checkout HEAD — src/main/)
$ echo “…” > test

This command is too long, therefore:

$ echo “./gradlew test” > runTests
$ echo “./gradlew build -x test” > buildIt
$ echo “git commit -am working” > commit
$ echo “git checkout HEAD — src/main/” > revert
$ echo “./buildIt && (./runTests && ./commit || ./revert)” > test
$ chmod +x buildIt commit revert runTests test

We have a very lean `test` now:

./buildIt && (./runTests && ./commit && ./revert)

This command does not delete our experiment:

@Test(expected = IllegalArgumentException.class)
public void fibNegative_illegal() {
Fib.fib(-1);
}
$ ./test

We can implement now the guard:

public static int fib(int n) {
if(n < 0)
throw new IllegalArgumentException();
if(n <= 2)
return 1;
return fib(n-1) + fib(n-2);
}

“Limbo lower now; Limbo lower now; How low can you go?”

Can we go lower? Yes, we can!

$ cat test
while true
do

./buildIt && (./runTests && ./commit || ./revert)
done

I created the Fibonacci project this way. What an experience. Every time, I made a mistake, some ghost hand pulled me back. It was almost, as I just paired with someone, who had an eye on my code!

Note: The tooling has to improve. IntelliJ, for example, did not synchronize from the filesystem immediately.

Conclusion

I was the opinion that TCR can’t work. But Kent Beck wrote on his article “Try it — it is cheap.” Therefore I did — and it worked. Just one day later, we discussed TCR in our Meetup-Group (Bavarian Coding Group), and since then, I am convinced, that TCR could be a new TDD (a new version).

I have never done a more significant project than Fibonacci, but I will. The experience is too good for not digging deeper. The collaboration aspect is also fascinating and worth exploring. Google Docs made the Proof of Concepts that Limbo is possible; now it is our time to proof (or proof the opposite!), that is is possible with Code.

Future posts — done

Future posts — planned

  • TCR and TDD, stateless vs. stateful?
  • How to use git stash to prevent Copy&Paste Pattern
  • Patterns in TCR
  • Collaboration example: Programming a Coffee-Maker
  • Integrate TCR in your IDE

A long example

It currently implement a Ray Tracer using TCR. You find the first post here. Until now, it works fine.

--

--

Thomas Deniffel

Programmer, CTO at Skytala GmbH, Software Craftsman, DDD, Passion for Technology