Windows Beta ver 1.7.1.7 Released

Finally, though a bit later than planned, we are pleased to announce the release of version 1.7.1.7 for Windows. We have meticulously tested the plugin to verify the following:

To assist with testing, we have developed automated tests to verify all detailed functions and compare them against expected results. These tests also run algorithms written in the ACF language to ensure they produce the correct outputs.

Additionally, we have a comprehensive library of ACF code that has been compiled, tested, and executed on Windows to verify consistent results. To move the plugin out of beta, we need more beta testers. We have launched a beta testing program, offering participants a 50% discount on a permanent license valid for both Windows and Mac installations. This means that your license will remain valid after the beta test period, for both platforms.

The beta version is available for download in our download area at horneks.no.

Need Assistance Getting Started?

Don't hesitate to contact us at any time for help. We will do whatever is necessary to get you up and running smoothly.

The automated tests

The automated tests is collected in one "ACF" file. First we have created a few assert functions. Those have three parameters, the two first is the actual result and the expected result. The third is a message to show if the two first are different.

// Assert strings. 
function assert ( string a1, string a2, string message )
    if ( a1 != a2 ) then
        throw "Error: " + format("%s, s1='%s', s2='%s'", message, a1, a2);
    end if
    return true; 
end

// Assert Floating point variables
function assertF ( float f1, float f2, string message )
    if ( round(f1,5) != round(f2,5)) then
        throw "Error: " + message + format (", f1=%f, f2=%f", f1, f2); 
    end if
    return true; 
end

// Assert integer or long integer variables. 
function assertL ( long L1, long L2, string message )
    if ( L1 != L2) then
        throw "Error: " + message + format (", L1=%ld, L2=%ld", l1, l2); 
    end if
    return true; 
end

When we use those, we have the calculation in its first parameter, and the expected result in parameter 2, then a message in the third parameter telling if the test fails. If the parameters are different, we throw an exception with the message.

Testing some math functions is done this way.

First, we need some functions to convert between radians and degrees. As the trigonometric functions operates on radians (2*PI = full circle) instead of degrees (360 = full circle).

function degreesToRadians(double degrees) 
    return degrees * (PI / 180.0);
end

function radiansToDegrees(double radians) 
    return radians * (180.0 / PI);
end

We are also using the CalcAnnuityInterestRate function as described in the introduction. This function calculates the actual interest rate for a given loan based on a fixed monthly payment. We have verified that this function calculates the correct interest rate.

Next, we proceed to the math tests, using the functions mentioned above.

function test_math ()
    bool a;
    try
        
        a = assertF(2*3+4*5, 26, "Priority of multiply versus addition failed");
        a = assertF(10/2-3, 2, "Division and subtraction priority failed");
        a = assertF(cos(degreesToRadians(60)), 0.5, "Cosine of 60 degrees failed");
        a = assertF(sin(degreesToRadians(30)), 0.5, "Sinus of 30 degrees failed");
        a = assertF(tan(degreesToRadians(45)), 1, "Tangens of 45 degrees failed");
        a = assertF(radiansToDegrees(acos(0.5)), 60, "Arccos of 0.5 failed");
        a = assertF(radiansToDegrees(asin(0.5)), 30, "ArcSin of 0.5 failed");
        a = assertF(radiansToDegrees(atan(1)), 45, "ArcTan of 1 failed");
        a = assertF(sqrt(16), 4, "SQRT failed");
        a = assertF(mod(10,7), 3, "Modulo failed");
        a = assertF(fact(5), 120, "Faculty failed");
        a = assertF(round(3.141592654, 3), 3.142, "Round failed");
        // We use the annuity interest rate binary search algorithm described in the intro in the manual to test
        // more calculations, repeat-until, if-then-else-endif and more. Let us call this function....
        a = assertF(CalcAnnuityInterestRate(100000, 2000, 5, 12) ,7.4200958013534545898, "Annuity calc test failed");
    
        ...
        ... (more tests)
    catch
        alert ( "Error: " + last_error); 
        a = false; 
    end try
    return a;   
end

The try/catch block show the message thrown in the assert function if there is some mismatch.

Similarly, here is some test of the string functions.

function test_strings ()
    bool a;
    int i; 
    array string arr1 = {"a", "b", "c"}, arr2; 
    date d1, d2; 
    try
        
        a = assert(left("abcdef", 3), "abc", "function 'left' failed");
        a = assert(right("abcdef", 3), "def", "function 'right' failed");
        a = assert(trimleft("  abcdef "), "abcdef ", "function 'trimleft' failed");
        a = assert(trimright("  abcdef "), "  abcdef", "function 'trimleft' failed");
        a = assert(trimboth("\r  abcdef   \n"), "abcdef", "function 'trimboth' failed");
        a = assert(upper("abcdefghijklmnopq"), "ABCDEFGHIJKLMNOPQ", "function 'upper' failed");
        a = assert(lower("ABCDEFGHIJKLMNOPQ"), "abcdefghijklmnopq", "function 'lower' failed");
        a = assert(proper("ABCDEFGH IJKLMNOPQ ola normann"), "Abcdefgh Ijklmnopq Ola Normann", "function 'proper' failed");
        a = assertL(pos("abcdefghijklmnopq","def"), 3, "Pos function failed"); // returns 0-based index
        a = assert(substitute("abcdefghijklmnopq", "ghi", "001"), "abcdef001jklmnopq", "Substitute failed"); 
        a = assert(mid("abcdefghijklmnopq",3,2), "de", "Function mid failed"); 
        a = assert(substring("abcdefghijklmnopq",3,2), "de", "Function substring failed"); 
        a = assertL(length("abcdef"), 6, "Function length failed"); 
        a = assertL(bytes("Bø"), 3, "Bytes on utf-8 string failed"); // 0x42 0xC3 0xB8
        a = assertL(length("Bø"), 2, "Length on utf-8 string failed"); 
        a = assertL(ascii("@"), 64, "Function ascii failed"); 
        a = assert(char(65), "A", "Function char failed"); 
        a = assertL(sizeof(arr1), 3, "Function sizeof failed"); 
        a = assert(implode(";", arr1), "a;b;c", "Function implode failed"); 
        arr2 = explode ( ";", "a;b;c"); 
        a = assertL ( sizeof (arr2), 3, "Dimension after explode is wrong"); 
        for (i= 1, 3)
            assert (arr1[i], arr2[i], format ("%s, Index %d", "Explode failed", i)); 
        end for
        a = assert (between ( "[start]abc[end]", "[start]", "[end]"), "abc", "Between failed"); 
        a = assertL(ValueCount ("a\rb\rc"), 3, "ValueCount failed"); 
        d1 = date ( "2024-01-02", "%Y-%m-%d"); 
        a = assert  (string ( d1, "%d.%m.%Y"), "02.01.2024", "Date to string with format failed"); 
        d2 = date ( "2024-01-10", "%Y-%m-%d"); 
        a = assertL(d2-d1, 8, "Date difference failed");
        a = assert  (string ( d1+10, "%d.%m.%Y"), "12.01.2024", "Date addition failed"); 
        a = assert  ("=!"*3, "=!=!=!", "String multiply (repeat) failed");
        a = assert  ("bbb"+"AAA", "bbbAAA", "String Concatenation failed");
        a = assert (testCase("tt"), "ttx3", "TestCase 1 Failed");
        a = assert (testCase("tx"), "txx3", "TestCase 2 Failed");
        a = assert (testCase("cc"), "ccxxccxxccxx3", "TestCase 3 Failed");
        a = assert (testCase("ff"), "defttdefttdeftt3", "TestCase 4 Failed");
    catch
        alert ( "Error: " + last_error); 
        a = false; 
    end try
    return a;   
end

And some Hash/HMAC functions.

function test_hashFunctions ()
    bool a; 
    string source = "Hello World!"; 
    string key = "my_secret_key";
    try
        // MD5
        a = assert(create_hash(md5, source, base64enc), "7Qdih1MuhjZehB6Sv8UNjA==", "MD5/Base64 failed");
        a = assert(create_hash(md5, source, hexenc), "ed076287532e86365e841e92bfc50d8c", "MD5/Hex failed");

        // SHA1
        a = assert(create_hash(sha1, source, base64enc), "Lve95gjOVATpfV8EL5X4nxwjKHE=", "SHA1/Base64 failed");
        a = assert(create_hash(sha1, source, hexenc), "2ef7bde608ce5404e97d5f042f95f89f1c232871", "SHA1/Hex failed");

        // SHA256
        a = assert(create_hash(sha256, source, base64enc), "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=", "SHA256/Base64 failed");
        a = assert(create_hash(sha256, source, hexenc), "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", "SHA256/Hex failed");

        // SHA384
        a = assert(create_hash(sha384, source, base64enc), "v9dsDrvQBv7lg0EFR8GIewKSvnbVgtlsJC0qeScj4/1v0GH51c/RO4+WE1jmrbpK", "SHA384/Base64 failed");
        a = assert(create_hash(sha384, source, hexenc), "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a", "SHA384/Hex failed");

        // SHA512
        a = assert(create_hash(sha512, source, base64enc), "hhhE1nBOhXP+w02WfiC8/vPUJM9IvgTm3AjyvVjHKXQzcQFerYkcw88cnTS0kmS1EHUbH/nlN5N7xGtdb/TsyA==", "SHA512/Base64 failed");
        a = assert(create_hash(sha512, source, hexenc), "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8", "SHA512/Hex failed");
        
        // HMAC MD5
        a = assert(create_hmac(md5, key, source, base64enc), "AYH2AOvu2Cbt3Ef/hR9k7A==", "Failed HMAC MD5/base64");
        a = assert(create_hmac(md5, key, source, hexenc), "0181f600ebeed826eddc47ff851f64ec", "Failed HMAC MD5/hex");
        
        // HMAC SHA1
        a = assert(create_hmac(sha1, key, source, base64enc), "BwpO356r2gx9SYj3UFVvc/BfdVA=", "Failed HMAC SHA1/base64");
        a = assert(create_hmac(sha1, key, source, hexenc), "070a4edf9eabda0c7d4988f750556f73f05f7550", "Failed HMAC SHA1/hex");

       // HMAC SHA256
        a = assert(create_hmac(sha256, key, source, base64enc), "5z1prOAxmam9lz5nlHgudpxIhksTY8pClCivq7RUytQ=", "Failed HMAC SHA256/base64");
        a = assert(create_hmac(sha256, key, source, hexenc), "e73d69ace03199a9bd973e6794782e769c48864b1363ca429428afabb454cad4", "Failed HMAC SHA256/hex");

       // HMAC SHA384
        a = assert(create_hmac(sha384, key, source, base64enc), "UDZdkjSVyMpFJQu9dmeAlKg+LW97o6yq1pfBj3QZJM4YMmgZxkoUVVUzyMDcP3DT", "Failed HMAC SHA384/base64");
        a = assert(create_hmac(sha384, key, source, hexenc), "50365d923495c8ca45250bbd76678094a83e2d6f7ba3acaad697c18f741924ce18326819c64a14555533c8c0dc3f70d3", "Failed HMAC SHA384/hex");

       // HMAC SHA512
        a = assert(create_hmac(sha512, key, source, base64enc), "x38L+mMFPEM+QqGrPMOzx2l7rTlmNePxPdaYllFiLQm+G9K2Bf/9oQcci861HAF4NTNqsPZS8gPEiQ0KoznMlA==", "Failed HMAC SHA512/base64");
        a = assert(create_hmac(sha512, key, source, hexenc), "c77f0bfa63053c433e42a1ab3cc3b3c7697bad396635e3f13dd6989651622d09be1bd2b605fffda1071c8bceb51c017835336ab0f652f203c4890d0aa339cc94", "Failed HMAC SHA512/hex");

    
    
    catch
        alert ( "Error: " + last_error); 
        a = false; 
    end try
    return a; 
end

More test will be updated later.

Testing with existing applications

There is developed quite a lot of applications based on the ACF plugin. Testing all those is an important task. They use all kinds of different functions, and see if they should work as expected.

Manual tests

Some tests is more manual, to example the OTP functionality. Here is the test procedure for this.

We need two small functions. One to generate a OTP secret, and the second to generate OTP code based on that secret.

Here is the functions:

/*

    Test OTP functions
    
*/

function genOTPSecret ()
    return generate_otp_secret();
end

function GenOTPcode ( string secret )
    return get_otp_code( secret );
end

Then, we use the Developemnt tool, compiles it and first run the genOTPSecret function. This gives you a Base32 encoded long string. In my example, I got `KDACNUJVSVGVDOHFJTSHMRZ54VCXBBKU´.

Then I paste this into Parameter 1 in the test tab, and run the GenOTPcode , resulting in a six-digit code in the result field.

To verify this, I installed the OTPManager app, and added a token to it, specifying the secret I got. Then in this app, I now started to get OTP codes, updated each 30 seconds with a new code. I ran the GenOPTcode function again, to see if the code matched what came from the OTPManger app. Testing several times on Windows, and the test matched each time.

Testing ZIP file functions.

This test is about to see that the file system functions works as expected also inside ZIP files.

I test packing a ZIP download for windows users.

Here is the ACF function used to test.

Function makeBetaTesterBoundle ( string name, string zipfile ) 

    string path = "C:\Users\Ole\Documents\BetaBundles\\"+zipfile;
    string z = open_zip ( path ) ; 
    int x;
    string text, res; 
    if ( left ( z, 3 ) == "ZIP" ) then
        res = copy_file ( "C:\Users\Ole\Desktop\WIndows-release-ACF-Plugin\ACF_Plugin.fmx64", z+"ACF-Plugin/ACF_Plugin.fmx64");
        print res+"\r\n";
        res = copy_file ( "C:\Users\Ole\Desktop\WIndows-release-ACF-Plugin\horneks-pub.cer",  z+"ACF-Plugin/horneks-pub.cer");
        print res+"\r\n";
        res = copy_file ( "c:\Prosjekter\curl\curl_release\bin\cacert.pem",  z+"ACF-Plugin/ssl/cacert.pem");
        print res+"\r\n";
        res = copy_file ( "C:\Users\Ole\Desktop\WIndows-release-ACF-Plugin\Release-notes.md",  z+"ACF-Plugin/Release-notes.md");
        print res+"\r\n";
        res = copy_file ( "F:\WIndows-release-ACF-Plugin\EditorBundles",  z+"ACF-Plugin");
        print res+"\r\n";
        res = copy_file ( "F:\WIndows-release-ACF-Plugin\Examples",  z+"ACF-Plugin");
        print res+"\r\n";
        res = copy_file ( "F:\WIndows-release-ACF-Plugin\Tools",  z+"ACF-Plugin");
        print res+"\r\n";
        
        x = open ( "C:\Users\Ole\Desktop\WIndows-release-ACF-Plugin\README.md", "r");
        text = read (x); 
        close (x);
        text = substitute ( text, "++name++", name); 
        x = open ( z + "ACF-Plugin/README.md", "w");
        write (x, text); 
        close (x);
        close_zip ( z ) ; 
        return "OK";
    else
        return "FAILED opening zip-file" + path; 
    end if
end

After that, we inspect the newly created ZIP archive to see if the content are what we expected.

The zip archive is available in the download section.