JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Shunt Swap 2-3

May 16, 2025 • [designluaobjectstesting]


This morning I plan to build the plan that swaps cars 2 and 3, since doing 1 and 2 worked yesterday. Then we probably need some design and refactoring.

We have this test running:

function Tests:test_reverse_two_via_plan()
   siding5 = Siding(5, "21345")
   siding31 = Siding(3)
   siding32 = Siding(3)
   loco = Locomotive(siding5, siding31, siding32)
   loco:plan_inversion_at_1()
   loco:execute_plan()
   self:assert_equals(loco:siding_car_string(1), "12345", "sorted!")
   self:assert_equals(loco:car_string(), "", "loco", "empty")
end

Let’s see if I can do the 2-3 inversion offhand. New test:

function Tests:test_reverse_two_three_via_plan()
   siding5 = Siding(5, "13245")
   siding31 = Siding(3)
   siding32 = Siding(3)
   loco = Locomotive(siding5, siding31, siding32)
   loco:plan_inversion_at_2()
   loco:execute_plan()
   self:assert_equals(loco:siding_car_string(1), "12345", "sorted!")
   self:assert_equals(loco:car_string(), "", "loco", "empty")
end

I renamed the other one to test_reverse_one_two_via_plan. Couldn’t think of a longer name. Now I need a new method. I’ll do the empty solution to get the test to fail.

function Locomotive:plan_inversion_at_2()
   self.plan = {}
end

I have to create an empty plan or the test will not run to completion. I should get a standard failure now.

[05:28] Inglenook 20250516: test_reverse_two_three_via_plan: 13245 was expected to  be 12345 (sorted!)

Perfect! Now let’s do the work. The sequence is rather like swapping 1 and 2, except that we pull an extra car all the time.

function Locomotive:plan_inversion_at_2()
   self.plan = {}
   self:add_action(self.move, 1)
   self:add_action(self.pull, 2) -- #1, #2 | #3
   self:add_action(self.move, 2)
   self:add_action(self.pull, 1) -- #1
   self:add_action(self.move, 1)
   self:add_action(self.pull, 2) -- #1, #3
   self:add_action(self.move, 2)
   self:add_action(self.pull, 3) -- #1 #3 #2
   self:add_action(self.move, 1)
   self:add_action(self.pull, 0)
end

As soon as I replaced my dots with colons, this worked first time.

Let’s take a more broad look at the situation. Here are some issues that I see right offhand:

Still, it would be good to have a better way to proceed than bubble sort. The trick is to recognize patterns and apply then in an order that reduces our total number of moves. We’ll see what we can figure out. Right now I feel good because I’m confident we can write a program that will at least solve the problem.

Let’s write a new method go_get to simplify our code, like this:

function Locomotive:go_get(siding_number, car_count)
   self:add_action(self.move, siding_number)
   self:add_action(self.pull, car_count)
end

Now we can use this in our existing plan methods:

function Locomotive:plan_inversion_at_2()
   self.plan = {}
   -- self:add_action(self.move, 1)
   -- self:add_action(self.pull, 2) -- #1, #2 | #3
   self:go_get(1, 2)
   -- self:add_action(self.move, 2)
   -- self:add_action(self.pull, 1) -- #1
   self:go_get(2, 1)
   -- self:add_action(self.move, 1)
   -- self:add_action(self.pull, 2) -- #1, #3
   self:go_get(1, 2)
   -- self:add_action(self.move, 2)
   -- self:add_action(self.pull, 3) -- #1 #3 #2
   self:go_get(2, 3)
   -- self:add_action(self.move, 1)
   -- self:add_action(self.pull, 0)
   self:go_get(1, 0)
end

Note that I commented out the old code and then inserted my new calls one after each pair, giving me a reasonable chance of getting it right. Save and see if this runs. And after I replace dots with colons, it does run. Fix up the other plan and remove the comments.

Along the way I decide to remove the test for the string-style plan. My new plan methods are nearly good:

function Locomotive:plan_inversion_at_1()
   self.plan = {}
   self:go_get(1, 1)
   self:go_get(2, 0)
   self:go_get(1, 1)
   self:go_get(2, 2)
   self:go_get(1, 0)
end

function Locomotive:plan_inversion_at_2()
   self.plan = {}
   self:go_get(1, 2)
   self:go_get(2, 1)
   self:go_get(1, 2)
   self:go_get(2, 3)
   self:go_get(1, 0)
end

But I noticed something odd. My test reports in chat look like this:

[05:58] Inglenook 20250516: Tests: correct 25, wrong 0
[06:01] Inglenook 20250516: Tests: correct 25, wrong 0
[06:05] Inglenook 20250516: test_first_inversion: nil was expected to  be m1 p1 home m2 p0 home m1 p1 home m2 p2 home m1 p0 home ()
[06:05] Inglenook 20250516: Tests: correct 24, wrong 1
[06:05] Inglenook 20250516: Tests: correct 26, wrong 0

Before I removed the string checking test, we had 25 correct, zero wrong. I removed the code to produce the string and that changed to 24 correct and one wrong, as one might expect. But when I removed the test, the report changed to 26 correct! I’d have expected 24 correct, zero wrong.

What’s up with this? We need to understand it, there is something going on that I can’t explain. This is bad. First, I put the test back. The report returns to 24, 1. Now let’s see if we have a verbose mode.

We have. It appears that test_reverse_two_three_via_plan is running twice with the test removed.

After some work trying to generate a bug report, I manage to isolate the problem to my code, which is good news.

This method:

function Tests:run_tests(verbose)
   test_correct = 0
   test_wrong = 0
   for k, v in Tests do
      if k:sub(1, 5) == 'test_' then
         Tests.current_name = k
         if verbose then print(k) end
         v(Tests)
      end
   end
   print(`Tests: correct {test_correct}, wrong {test_wrong}`)
end

The line that sets current_name is adding a key to the table Tests, inside a loop over the table! This is an explicitly improper thing to do. You can change a key’s value, and I think you can even remove a key, inside the loop, but you cannot add keys. The fix is to initialize it in the original creation of Tests:

Tests = {current_name=""}

And we only see each test run once. Well-spotted, Janet!

Let’s close this out on a couple of high notes.

High Notes

I found a bug in my testing framework. In play it caused a test to run twice, but I suspect it could just as easily cause a test to be skipped. Or explode a kitten: the behavior is undefined. I found it by removing all the content of all the tests and just looking to see if the test name printed twice. I was preparing a bug report for LL, so I wanted to trim things down as far as I could.

I removed all the classes and the class function, still the duplicate test. Then I removed one of the methods of Tests, I think diagnose, and suddenly the test didn’t appear. I put it back. Enough fiddling like that and I finally noticed the setting of the string name into the Tests table. Initialized that and all was good.

I am very confident that I have found the issue. I am not bet your life confident, because I never am, but I think that was the issue. Certainly it was the only thing “wrong” in my code and fixing it fixed the problem. Since it’s a Heisenbug by its nature, we can’t be sure we got it.

More the point of the day, another inversion can be uninverted, and the code is a bit more compact with the new ge_get method. I suspect it could be even more compact, and as we code up more primitives we’ll keep a look out for opportunities to simplify our work while keeping the code understandable.

A good morning. Time for some caffeine and a bit of reading. Until next time, I wish you safe paths!