To alleviate the grind of polishing and sanitising my code (and, let’s be honest, just plain ‘fixing’ it in some cases) ready for release, I have re-kindled my participation on Stack Overflow. In a happy confluence yesterday a question came up which allowed me to exercise one of the libraries that I’m preparing to release: Smoketest.

The question wasn’t a complicated one. It was an obvious case of using a pre-Unicode library with Unicode versions of Delphi and the problems that arise.

The excuse to use my Smoketest library came when the OP complained that my suggested solution had not worked. I admit, I had not tested my suggestion at the time that I made it. The approach suggested was simple enough and should have worked. But I decided to dog-food the suggestion by downloading the library in question and putting together a Smoketest for it.

The approach was simply this:

  • Build a test using the original library.
  • Run the test in Delphi 2006 and 2010*1
  • Apply my suggested changes to the AES library
  • Re-run the tests

*1 The OP was using Delphi 7 and XE but the principle was the same: different, pre-Unicode and Unicode versions of Delphi.

So, to begin at the beginning…

Making the Case for Testing

Writing test cases with Smoketest is very simple.

I use the Deltics.Smoketest unit, derive at least one Test Case class with one or more test methods, add that test case to the Smoketest and then declare my test ready to run.

In this case since it is a very simple test scenario everything can go in the DPR so I also use the two units I am actively testing and implement three separate methods, one for each type of string I am testing:

program TestAES;

uses
  ElAES,
  AES,
  Deltics.Smoketest;

type
  TTest = class(TTestCase)
    procedure WithANSIString;
    procedure WithString;
    procedure WithWideString;
  end;

  procedure TTest.WithANSIString;
  begin
  end;

  procedure TTest.WithString;
  begin
  end;

  procedure TTest.WithWideString;
  begin
  end;


begin
  Smoketest.Add([TTest]);
  Smoketest.Ready;
end.

Running this I get a ready to run test case:

Ready to Run
Ready to Run

As you can see, Smoketest tries to prettify names in the suite for human consumption (there are hooks provided to enable the test author to provide specific names for certain things if desired).

We can also see that tests have not been run at this point, although if I specify a command-line start parameter (-r) I can have all or just some of my tests run automatically at startup. I’ll add this to save some time when I re-run these tests and at this point running the tests yields the following:

Work in Progress
Work in Progress

The little “men at work” icons indicate test methods that produce no output and have no result. In other words, tests still to be written. But we knew that. 🙂

So now I write those tests. First, the WithANSIString test.

Vectors and Inspections

Bear in mind that I am not endeavouring to create a comprehensive test suite for the entire AES library, only to exercise enough of it to validate that a suggested “ANSIfication” approach is valid. One simple function from the library should suffice: EncryptString()

At this point I don’t have a test vector for this function. That is, I don’t know what the expected result is.

Ordinarily in such cases I would track down some suitable vectors to incorporate in my test, but on this occasion I will assume that when working with ANSI Strings the library provides correct results so initially I will make my WithANSIString test a simple inspection test so that I can see what the result is.

I will use constants for the KEY and the VALUE provided to the EncryptString() function that I will use for this test, entirely arbitrary values.

  const
    KEY    = 'jalphi';
    VALUE  = 'password';
    RESULT = 'TBA'; 

  procedure TTest.WithANSIString;
  var
    s: ANSIString;
  begin
    s := VALUE;
    Inspect('sample').Value(EncryptString(s, KEY));
  end;

A technique I have used throughout Smoketest is to chain methods that return interfaces to allow tests to be written using near natural language. It’s similar to so-called “fluent” API’s, but on this occasion intended to approximate a “domain specific language”, guiding the correct construction of tests rather than simply offering convenience.

In the case of an inspection, we start with an Inspect method which (optionally) takes a parameter providing a descriptive label for the value being inspected. The Inspect method yields an IInspector interface which provides a host of overloaded methods for capturing a value to be inspected.

If you wish, you can create your own custom Inspector interfaces (and implementations) and once registered with Smoketest you can obtain them using the “as” operator on the built-in inspector:

(Inspect('exotic value') as IMyExoticInspector).Value(MyExotica);

But this is advanced Smoketest voodoo and not necessary for this simple scenario. 🙂

Running the test case in this state now provides a useful result in the output of the first test. The remaining two tests are still “under construction”.

What's the Vector, Victor ?
What’s the Vector, Victor ?

We now have a test result to work with.

Great Expectations

I can now revise my WithANSIString test so that it is an actual test. The further two test methods are almost exact duplicates of this test. Only the declared type of our s string changes in each case, the input to the EncryptString() function:

  const
    KEY     = 'jalphi';
    VALUE   = 'password';
    RESULT  = '0800000000000000DEC4FFE94BD6F4396BCAD729D0C905A5';

  procedure TTest.WithANSIString;
  var
    s: ANSIString;
  begin
    s := VALUE;
    Test('result').Expect(EncryptString(s, KEY)).Equals(RESULT);
  end;

  procedure TTest.WithNativeString;
  var
    s: String;
  begin
    s := VALUE;
    Test('result').Expect(EncryptString(s, KEY)).Equals(RESULT);
  end;

  procedure TTest.WithWideString;
  var
    s: WideString;
  begin
    s := VALUE;
    Test('result').Expect(EncryptString(s, KEY)).Equals(RESULT);
  end;

Writing tests is similar to writing inspections. Instead of an Inspect method, for a test we start with the Test method. Rather than an IInspector, this yields an ITest and instead of simply identifying a Value() method we now begin our test by identifying something about which we have an expectation that is to be met, using the Expect() method.

The Expect() method is overloaded similar to the Value() method on an Inspector, but in this case there is more that has to be identified. Each overloaded version of Expect() returns an expectation interface specific to the type of value involved, providing the expectations that might apply to a value of that type.

So for a string value we have the following expectations available to us:

Great Expectations
Great Expectations

The expectations on an integer value would be different. If a value has qualities that might be better tested with some other expectation these expectations can be exposed as part of the expectation interface.

In the case of strings for example, if we were concerned with the specific length of a string we can write:

  Test('result length').Expect(s).Length.NotLessThan(100);

We started with IStringExpectations, but the Length of the string is exposed in those expectations itself as a set of IIntegerExpectations. We could of course have started with an integer expection more directly by using the Length() function on the string to start with:

  // These tests are functionally identical:

  Test('result length').Expect(Length(s)).NotLessThan(100);
  Test('result length').Expect(s).Length.NotLessThan(100);

But you at least have the choice. 🙂

Similarly, but perhaps more usefully, the default behaviour of string expectations is to act case sensitively. If we have expectations which are not dependent upon case then we can obtain CaseInsensitive expectations.

  Test('result length').Expect(s).CaseInsensitive.Equals(RESULT)

You may also have noticed that expectations in turn yield an IEvaluation interface. This is used in the self-tests of Smoketest itself to identify tests that are expected to fail (i.e. where expectations not bein met are actually the hoped for result!) but can also be used to abort a test case. So for example you can append “IsRequired” to an expectation and if the test fails then the test method will halt at that point. If you append “IsCritical” then the entire test case of which the method is a part will halt.

You would use this when testing that you have successfully obtained an object reference that is subsequently used in a test:

  widget := WidgetManager.LoadWidget;
  Test('Got a widget').Expect(widget).IsAssigned.IsRequired;

  widget.FlipTheDoodah;  // << It's OK, we won't reach this point if we didn't get a widget

Again, all of this is far more advanced stuff than we need in this simple case. 🙂

Back to our simple test and all three test methods have passed (YAY!) and we can get a summary of the test outcomes for each test. In this case there is just one outcome in each test, as expected:

Sweet Smell of Success
Sweet Smell of Success

Expected Failure

Opening the test project in Delphi 2010, rebuilding and running we also get the expected result, but in this case that is failure. Complete and abject.

Complete and Utter Failure
Complete and Utter Failure

But this is good. This confirms what the OP on StackOverflow originally reported – that the AES library was giving different, incorrect results when compiled on Unicode versions of Delphi.

So I can now apply the change I suggested to the AES library and see if the OP is also right in saying that this didn’t work.

Sweet Smell of Success
Sweet Smell of Success

After doing that, rebuilding in Delphi 2010 and re-running the tests I see straight away that if the OP is still seeing problems then there is something else involved in his case since the ANSIfication of that library results in consistent correct results not only in Delphi 2010 also still in Delphi 2006.

Huzzah!

I can only hope that the StackOverflow questioner eventually reaches a similarly happy conclusion. 🙂

1 thought on “Blowing Smoke…

  1. Nice worked-out case. You may want to point the OP from the OS question to this article so that he sees how he could set up tests for this own conversion work.

Comments are closed.