https://bitbucket.org/daniel_fort/magic-lantern
Raw File
Tip revision: 035e8fee788cf3127ae7fa7ddd5a7489d1c6b985 authored by Daniel Fort on 04 May 2018, 00:43:52 UTC
changes from update-to-6D.118 branch
Tip revision: 035e8fe
selftest.c
#include <module.h>
#include <dryos.h>
#include <bmp.h>
#include <menu.h>
#include <beep.h>
#include <lens.h>
#include <shoot.h>
#include <zebra.h>
#include <propvalues.h>
#include <timer.h>
#include <console.h>
#include <ml_rpc.h>
#include <edmac.h>
#include <edmac-memcpy.h>
#include <screenshot.h>
#include <powersave.h>
#include <focus.h>
#include <alloca.h>

/* optional routines */
extern WEAK_FUNC(ret_0) uint32_t ml_rpc_send(uint32_t command, uint32_t parm1, uint32_t parm2, uint32_t parm3, uint32_t wait);
extern WEAK_FUNC(ret_0) void ml_rpc_verbose(uint32_t state);
extern WEAK_FUNC(ret_0) void expfuse_preview_update_task(int direction);
extern WEAK_FUNC(ret_0) void playback_compare_images_task(int direction);

/* macros are used to get the names from consts.h in the stub test log */
#define MALLOC_FREE_MEMORY GetFreeMemForMalloc()
#define DISPLAY_IS_ON display_is_on()
#define PLAY_MODE is_play_mode()
#define MENU_MODE is_menu_mode()
#define HALFSHUTTER_PRESSED get_halfshutter_pressed()
#define CURRENT_GUI_MODE get_gui_mode()

/* button codes */
static int BGMT_PLAY;
static int BGMT_MENU;
static int BGMT_INFO;
static int BGMT_LV;
static int BGMT_PRESS_SET;
static int BGMT_WHEEL_LEFT;
static int BGMT_WHEEL_RIGHT;
static int BGMT_WHEEL_UP;
static int BGMT_WHEEL_DOWN;
static int BGMT_TRASH;

/* some private functions that should not be called from user code */
extern void* __priv_malloc(size_t size);
extern void  __priv_free(void* ptr);
extern void* __priv_AllocateMemory(size_t size);
extern void  __priv_FreeMemory(void* ptr);
extern void* __priv_alloc_dma_memory(size_t size);
extern void  __priv_free_dma_memory(void* ptr);
extern void* __priv_shoot_malloc(size_t size);
extern void  __priv_shoot_free(void* ptr);
#define _malloc __priv_malloc
#define _free __priv_free
#define _AllocateMemory __priv_AllocateMemory
#define _FreeMemory __priv_FreeMemory
#define _alloc_dma_memory __priv_alloc_dma_memory
#define _free_dma_memory __priv_free_dma_memory
#define _shoot_malloc __priv_shoot_malloc
#define _shoot_free __priv_shoot_free


static void stress_test_picture(int n, int delay)
{
    if (shutter_count > 20000)
    {
        NotifyBox(2000, "Skipping picture taking test");
        msleep(2000);
        beep();
        return;
    }
    
    msleep(delay);
    for (int i = 0; i < n; i++)
    {
        NotifyBox(10000, "Picture taking: %d/%d", i+1, n);
        msleep(200);
        lens_take_picture(64, 0);
    }
    lens_wait_readytotakepic(64);
    msleep(delay);
}

static volatile int timer_func = 0;
static volatile int timer_arg = 0;
static volatile int64_t timer_time = 0;

static void timer_cbr(int arg1, void* arg2)
{
    timer_func = 1;
    timer_arg = arg1;
    timer_time = get_us_clock();
}

static void overrun_cbr(int arg1, void* arg2)
{
    timer_func = 2;
    timer_arg = arg1;
    timer_time = get_us_clock();
}

static void next_tick_cbr(int arg1, void* arg2)
{
    timer_func = 3;
    timer_arg = arg1;
    timer_time = get_us_clock();
    SetHPTimerNextTick(arg1, 100000, timer_cbr, overrun_cbr, 0);
}

#define TEST_MSG(fmt, ...) { if (!stub_silence || !stub_ok) { stub_log_len += snprintf(stub_log_buf + stub_log_len, stub_max_log_len - stub_log_len, fmt, ## __VA_ARGS__); printf(fmt, ## __VA_ARGS__); } }
#define TEST_VOID(x) { x; stub_ok = 1; TEST_MSG("       %s\n", #x); }
#define TEST_FUNC(x) { int ans = (int)(x); stub_ok = 1; TEST_MSG("       %s => 0x%x\n", #x, ans); }
#define TEST_FUNC_CHECK(x, condition) { int ans = (int)(x); stub_ok = ans condition; TEST_MSG("[%s] %s => 0x%x\n", stub_ok ? "Pass" : "FAIL", #x, ans); if (stub_ok) stub_passed_tests++; else stub_failed_tests++; }
#define TEST_FUNC_CHECK_STR(x, expected_string) { char* ans = (char*)(x); stub_ok = streq(ans, expected_string); TEST_MSG("[%s] %s => '%s'\n", stub_ok ? "Pass" : "FAIL", #x, ans); if (stub_ok) stub_passed_tests++; else { stub_failed_tests++; msleep(500); } }

static char * stub_log_buf = 0;
static int stub_log_len = 0;
static int stub_max_log_len = 1024*1024;
static int stub_silence = 0;
static int stub_ok = 1;
static int stub_passed_tests = 0;
static int stub_failed_tests = 0;

static void stub_test_edmac()
{
    int size = 8*1024*1024;
    uint32_t *src, *dst;
    TEST_FUNC_CHECK(src = fio_malloc(size), != 0);
    TEST_FUNC_CHECK(dst = fio_malloc(size), != 0);

    if (src && dst)
    {
        /* fill source data */
        for (int i = 0; i < size/4; i++)
        {
            src[i] = rand();
        }

        /* force a fallback to memcpy */
        TEST_FUNC_CHECK(memcmp(dst, src, 4097), != 0);
        TEST_FUNC_CHECK(edmac_memcpy(dst, src, 4097), == (int) dst);
        TEST_FUNC_CHECK(memcmp(dst, src, 4097), == 0);
        TEST_FUNC_CHECK(edmac_memcpy(dst, src, 4097), == (int) dst);

        /* use fast EDMAC copying */
        TEST_FUNC_CHECK(memcmp(dst, src, size), != 0);
        TEST_FUNC_CHECK(edmac_memcpy(dst, src, size), == (int) dst);
        TEST_FUNC_CHECK(memcmp(dst, src, size), == 0);

        /* fill source data again */
        for (int i = 0; i < size/4; i++)
        {
            src[i] = rand();
        }

        /* abort in the middle of copying */
        TEST_FUNC_CHECK(memcmp(dst, src, size), != 0);
        TEST_FUNC_CHECK(edmac_memcpy_start(dst, src, size), == (int) dst);

        /* fixme: global */
        extern uint32_t edmac_write_chan;

        /* wait until the middle of the buffer */
        /* caveat: busy waiting; do not use in practice */
        /* here, waiting for ~10ms may be too much, as EDMAC is very fast */
        uint32_t mid = (uint32_t)CACHEABLE(dst) + size / 2;
        uint64_t t0 = get_us_clock();
        while (edmac_get_pointer(edmac_write_chan) < mid)
            ;
        uint64_t t1 = get_us_clock();

        /* stop here */
        AbortEDmac(edmac_write_chan);

        /* report how long we had to wait */
        int dt = t1 - t0;
        TEST_FUNC(dt);

        /* how much did it copy? */
        int copied = edmac_get_pointer(edmac_write_chan) - (uint32_t)CACHEABLE(dst);
        TEST_FUNC_CHECK(copied, >= size/2);
        TEST_FUNC_CHECK(copied, < size*3/2);

        /* did it actually stop? */
        msleep(100);
        int copied2 = edmac_get_pointer(edmac_write_chan) - (uint32_t)CACHEABLE(dst);
        TEST_FUNC_CHECK(copied, == copied2);

        /* did it copy as much as it reported? */
        TEST_FUNC_CHECK(memcmp(dst, src, copied), == 0);
        TEST_FUNC_CHECK(memcmp(dst, src, copied + 16), != 0);
        TEST_VOID(edmac_memcpy_finish());
    }

    TEST_VOID(free(src));
    TEST_VOID(free(dst));
}

/* delay with interrupts disabled */
static void busy_wait_ms(int ms)
{
    int t0 = get_ms_clock();
    while (get_ms_clock() - t0 < ms)
        ;
}

/* this checks whether clean_d_cache actually writes the data to physical memory
 * so other devices (such as DMA controllers) will see the same memory contents as the CPU */
static void stub_test_cache_bmp()
{
    TEST_MSG("Cache test A (EDMAC on BMP buffer)...\n");

    void * bmp;
    TEST_FUNC_CHECK(bmp = bmp_load("ML/CROPMKS/CINESCO2.BMP", 1), != 0);
    if (!bmp) return;

    uint8_t * const bvram_mirror = get_bvram_mirror();
    if (!bvram_mirror) return;

    /* perform the test twice:
     * one without cache cleaning, expected to fail,
     * and one with cache cleaning, expected to succeed */
    for (int k = 0; k < 2; k++)
    {
        /* perform this test with interrupts disabled */
        int old = cli();
        int dis = cli();
        TEST_FUNC_CHECK(old, != dis);

        /* draw a cropmark */
        clrscr();
        bmp_draw_scaled_ex(bmp, os.x0, os.y0, os.x_ex, os.y_ex, bvram_mirror);

        /* copy the image to idle buffer using EDMAC */
        uint8_t * src = bmp_vram_real();
        uint8_t * dst = bmp_vram_idle();

        ASSERT(src == CACHEABLE(src));
        ASSERT(dst == CACHEABLE(dst));

        if (k == 0)
        {
            /* trick the copying routine so it doesn't handle caching issues.
             * these pointers are actually cacheable (for speed reasons);
             * flagging them as uncacheable has no effect on DMA behavior.
             * this test should fail. */
            src = UNCACHEABLE(src);
        }

        /* mark destination as uncacheable (the EDMAC routine expects it this way) */
        /* this is generally incorrect; you should use fio_malloc instead. */
        dst = UNCACHEABLE(dst);

        edmac_copy_rectangle_adv_start(dst, src, 960, 0, 0, 960, 0, 0, 720, 480);

        /* wait for EDMAC transfer to finish */
        /* probably not needed, as take_semaphore will re-enable interrupts */
        busy_wait_ms(1000);

        /* cleanup EDMAC transfer */
        edmac_copy_rectangle_finish();

        /* interrupts are disabled again - from DryOS context switch */
        /* now compare the image buffers */
        int differences = 0;
        for (int y = 0; y < 480; y++)
        {
            for (int x = 0; x < 720; x++)
            {
                int i = x + y * 960;
                if (src[i] != dst[i])
                {
                    differences++;
                }
            }
        }

        info_led_on();
        busy_wait_ms(1000);
        info_led_off();

        /* do we still have interrupts disabled? */
        int irq = cli();
        TEST_FUNC_CHECK(irq, == dis);

        if (k)
        {
            /* expect to succeed */
            TEST_FUNC_CHECK(differences, == 0);
        }
        else
        {
            /* expect to fail */
            TEST_FUNC_CHECK(differences, > 100);
        }

        /* interrupts no longer needed */
        sei(old);
    }
}

static int stub_test_cache_fio_do(int handle_cache)
{
    /* prefer CF card if present */
    char * test_file = is_dir("A:/") ? "A:/test.dat" : "test.dat";
    FILE * f;
    TEST_FUNC_CHECK(f = FIO_CreateFile(test_file), != 0);

    /* result */
    int fail = -1;

    /* cacheable buffer that will fit the entire cache */
    /* placed at some random stack offset */
    /* note: we have 32K stack */
    const int size = 8192;
    uint8_t * pad = alloca(MOD(rand(), size));
    uint8_t * buf = alloca(size);

    /* make sure pad gets allocated above buf
     * therefore moving "buf" on the stack at some random offset */
    ASSERT(buf + 8192 <= pad);

    /* fill the buffer (this should bring it into cache) */
    for (int i = 0; i < size; i++)
    {
        buf[i] = i;
    }

    /* fill the buffer again; hoping some values won't reach the physical memory */
    for (int i = 0; i < size; i++)
    {
        buf[i] = i + 1;
    }

    /* save it to card */
    if (handle_cache & 1)
    {
        /* let the wrapper handle the cacheable buffer */
        TEST_FUNC_CHECK(FIO_WriteFile(f, buf, size), == size);
    }
    else
    {
        /* Trick the wrapper into calling Canon stub directly,
         * so it will no longer clean the cache before writing.
         * This should fail - do not use it in practice.
         * The uncacheable flag has no effect on DMA.
         * You should either use fio_malloc (which returns uncacheable buffers)
         * or pass regular (cacheable) pointers and let the wrapper handle them. */
        TEST_FUNC_CHECK(FIO_WriteFile(f, UNCACHEABLE(buf), size), == size);
    }

    TEST_VOID(FIO_CloseFile(f));

    /* reopen the file for reading */
    TEST_FUNC_CHECK(f = FIO_OpenFile(test_file, O_RDONLY | O_SYNC), != 0);

    /* read the file into uncacheable memory (this one will be read correctly) */
    uint8_t * ubuf = fio_malloc(size);
    TEST_FUNC_CHECK(ubuf, != 0);
    if (!ubuf) goto cleanup;

    TEST_FUNC_CHECK(FIO_ReadFile(f, ubuf, size), == size);
    FIO_SeekSkipFile(f, 0, SEEK_SET);

    /* alter the buffer contents; hoping some values will be only in cache */
    for (int i = 0; i < size; i++)
    {
        buf[i] = i + 2;
    }

    /* read the file into regular (cacheable) memory */
    if (handle_cache & 2)
    {
        /* let the wrapper handle the cacheable buffer */
        TEST_FUNC_CHECK(FIO_ReadFile(f, buf, size), == size);
    }
    else
    {
        /* Trick the wrapper into calling Canon stub directly.
         * This should fail (same as with FIO_WriteFile). */
        TEST_FUNC_CHECK(FIO_ReadFile(f, UNCACHEABLE(buf), size), == size);
    }

    /* check the results */
    int a = 0, b = 0, c = 0, r = 0;
    for (int i = 0; i < size; i++)
    {
        a += (ubuf[i] == (uint8_t)(i));
        b += (ubuf[i] == (uint8_t)(i + 1));
        c += (ubuf[i] == (uint8_t)(i + 2));
        r += (ubuf[i] == buf[i]);
    }

    free(ubuf);

    /* don't report success/failure yet, as this test is not deterministic
     * just log the values and return the status */
    TEST_FUNC(a);
    TEST_FUNC(b);
    TEST_FUNC(c);
    TEST_FUNC(a + b + c);
    TEST_FUNC(r);

    /* which part of the test failed? read or write? */
    int fail_r = (r != size);
    int fail_w = (a != 0) || (b != size) || (c != 0);
    fail = (fail_r << 1) | fail_w;

cleanup:
    /* cleanup */
    TEST_VOID(FIO_CloseFile(f));
    TEST_FUNC_CHECK(FIO_RemoveFile(test_file), == 0);
    return fail;
}

static void stub_test_cache_fio()
{
    TEST_MSG("Cache test B (FIO on 8K buffer)...\n");

    /* non-deterministic test - run many times */
    stub_silence = 1;

    int tries[4] = {0};
    int times[4] = {0};
    int failr[4] = {0};
    int failw[4] = {0};

    for (int i = 0; i < 1000; i++)
    {
        /* select whether the FIO_WriteFile wrapper (1) and/or
         * FIO_ReadFile (2) wrapper should handle caching issues */
        int handle_cache = rand() & 3;

        /* run one iteration and time it */
        int t0 = get_ms_clock();
        int fail = stub_test_cache_fio_do(handle_cache);
        int t1 = get_ms_clock();
        ASSERT(fail == (fail & 3));

        /* count the stats */
        tries[handle_cache]++;
        times[handle_cache] += (t1 - t0);
        if (fail & 1) failw[handle_cache]++;
        if (fail & 2) failr[handle_cache]++;

        /* progress indicator */
        if (i % 100 == 0)
        {
            printf(".");
        }
    }
    stub_silence = 0;
    printf("\n");

    /* report how many tests were performed in each case */
    TEST_FUNC_CHECK(tries[0], > 100);
    TEST_FUNC_CHECK(tries[1], > 100);
    TEST_FUNC_CHECK(tries[2], > 100);
    TEST_FUNC_CHECK(tries[3], > 100);

    /* each test (read or write) should succeed only
     * if the corresponding wrapper (FIO_WriteFile
     * and FIO_ReadFile) is allowed to handle caching
     * for regular buffers; it should fail otherwise,
     * at least a few times. This also implies both tests
     * (R and W) should succeed if and only if both routines
     * are allowed to handle caching. */
    TEST_FUNC_CHECK(failr[0], > 10);
    TEST_FUNC_CHECK(failw[0], > 10);
    TEST_FUNC_CHECK(failr[1], > 10);
    TEST_FUNC_CHECK(failw[1], == 0);
    TEST_FUNC_CHECK(failr[2], == 0);
    TEST_FUNC_CHECK(failw[2], > 10);
    TEST_FUNC_CHECK(failr[3], == 0);
    TEST_FUNC_CHECK(failw[3], == 0);

    /* check whether cache cleaning causes any slowdown */
    TEST_FUNC(times[0] / tries[0]);
    TEST_FUNC(times[1] / tries[1]);
    TEST_FUNC(times[2] / tries[2]);
    TEST_FUNC(times[3] / tries[3]);
}

static void stub_test_cache()
{
    stub_test_cache_bmp();
    stub_test_cache_fio();

    TEST_MSG("Cache tests finished.\n\n");
}

static int wait_focus_status(int timeout, int value)
{
    int t0 = get_ms_clock();

    while (get_ms_clock() - t0 < timeout)
    {
        msleep(10);

        if (lv_focus_status == value)
        {
            return 1;
        }
    }
    return 0;
}

static void stub_test_af()
{
    /* Autofocus (with or without LiveView) */
    int lv0 = lv;

    /* test this loop 10 times */
    for (int k = 0; k < 10; k++)
    {
        if (k < 5) force_liveview();
        else close_liveview();

        while (is_manual_focus())
        {
            NotifyBox(2000, 
                "Please enable autofocus.\n"
                "Press half-shutter to skip."
            );
            msleep(1000);

            if (HALFSHUTTER_PRESSED)
            {
                return;
            }
        }

        /* assume half-shutter is not pressed before starting the test */
        TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 0);

        /* enable autofocus on half-shutter */
        /* lv_focus_status expected to be 3 when focusing and 1 or 2 when idle */
        lens_setup_af(AF_ENABLE);
        module_send_keypress(MODULE_KEY_PRESS_HALFSHUTTER);
        msleep(200);
        TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 1);
        if (lv) {
            TEST_FUNC_CHECK(wait_focus_status(1000, 3), == 1);
            TEST_FUNC_CHECK(lv_focus_status, == 3)
        } else {
            msleep(1000);
            TEST_FUNC_CHECK(get_focus_confirmation(), != 0);
        }

        module_send_keypress(MODULE_KEY_UNPRESS_HALFSHUTTER);
        msleep(500);
        TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 0);
        if (lv) {
            TEST_FUNC_CHECK(wait_focus_status(1000, 3), == 0);
            TEST_FUNC_CHECK(lv_focus_status, != 3)
        } else {
            TEST_FUNC_CHECK(get_focus_confirmation(), == 0);
        }
        lens_cleanup_af();

        /* disable autofocus on half-shutter */
        /* this time, autofocus should fail */
        lens_setup_af(AF_DISABLE);
        module_send_keypress(MODULE_KEY_PRESS_HALFSHUTTER);
        TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 1);
        if (lv)
        {
            TEST_FUNC_CHECK(wait_focus_status(1000, 3), == 0);
        }
        else
        {
            msleep(1000);
            TEST_FUNC_CHECK(get_focus_confirmation(), == 0);
        }
        module_send_keypress(MODULE_KEY_UNPRESS_HALFSHUTTER);
        msleep(500);
        TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 0);
        TEST_FUNC_CHECK(lv_focus_status, != 3)
        lens_cleanup_af();
    }

    if (lv0)
    {
        /* if the test was started from LiveView, return to LV */
        force_liveview();
    }
}

static void stub_test_file_io()
{
    /* File I/O */
    FILE* f;
    TEST_FUNC_CHECK(f = FIO_CreateFile("test.dat"), != 0);
    TEST_FUNC_CHECK(FIO_WriteFile(f, (void*)0xFF000000, 0x10000), == 0x10000);
    TEST_FUNC_CHECK(FIO_WriteFile(f, (void*)0xFF000000, 0x10000), == 0x10000);
    TEST_VOID(FIO_CloseFile(f));
    uint32_t size;
    TEST_FUNC_CHECK(FIO_GetFileSize("test.dat", &size), == 0);
    TEST_FUNC_CHECK(size, == 0x20000);
    void* p;
    TEST_FUNC_CHECK(p = (void*)_alloc_dma_memory(0x20000), != 0);
    TEST_FUNC_CHECK(f = FIO_OpenFile("test.dat", O_RDONLY | O_SYNC), != 0);
    TEST_FUNC_CHECK(FIO_ReadFile(f, p, 0x20000), == 0x20000);
    TEST_VOID(FIO_CloseFile(f));
    TEST_VOID(_free_dma_memory(p));

    {
        int count = 0;
        FILE* f = FIO_CreateFile("test.dat");
        if (f)
        {
            for (int i = 0; i < 1000; i++)
                count += FIO_WriteFile(f, "Will it blend?\n", 15);
            FIO_CloseFile(f);
        }
        TEST_FUNC_CHECK(count, == 1000*15);
    }
    
    /* FIO_SeekSkipFile test */
    {
        void* buf = 0;
        TEST_FUNC_CHECK(buf = fio_malloc(0x1000000), != 0);
        memset(buf, 0x13, 0x1000000);
        if (buf)
        {
            /* create a file a little higher than 2 GiB for testing */
            /* to make sure the stub handles 64-bit position arguments */
            FILE* f = FIO_CreateFile("test.dat");
            if (f)
            {
                printf("Creating a 2GB file...       ");
                for (int i = 0; i < 130; i++)
                {
                    printf("\b\b\b\b\b\b\b%3d/130", i);
                    FIO_WriteFile(f, buf, 0x1000000);
                }
                printf("\n");
                FIO_CloseFile(f);
                TEST_FUNC_CHECK(FIO_GetFileSize_direct("test.dat"), == (int)0x82000000);
                
                /* now reopen it to append something */
                TEST_FUNC_CHECK(f = FIO_OpenFile("test.dat", O_RDWR | O_SYNC), != 0);
                TEST_FUNC_CHECK(FIO_SeekSkipFile(f, 0, SEEK_END), == (int)0x82000000);
                TEST_FUNC_CHECK(FIO_WriteFile(f, buf, 0x10), == 0x10);

                /* some more seeking around */
                TEST_FUNC_CHECK(FIO_SeekSkipFile(f, -0x20, SEEK_END), == (int)0x81fffff0);
                TEST_FUNC_CHECK(FIO_WriteFile(f, buf, 0x30), == 0x30);
                TEST_FUNC_CHECK(FIO_SeekSkipFile(f, 0x20, SEEK_SET), == 0x20);
                TEST_FUNC_CHECK(FIO_SeekSkipFile(f, 0x30, SEEK_CUR), == 0x50);
                TEST_FUNC_CHECK(FIO_SeekSkipFile(f, -0x20, SEEK_CUR), == 0x30);
                
                /* note: seeking past the end of a file does not work on all cameras, so we'll not test that */

                FIO_CloseFile(f);
                TEST_FUNC_CHECK(FIO_GetFileSize_direct("test.dat"), == (int)0x82000020);
            }
        }
        fio_free(buf);
    }

    TEST_FUNC_CHECK(is_file("test.dat"), != 0);
    TEST_FUNC_CHECK(FIO_RemoveFile("test.dat"), == 0);
    TEST_FUNC_CHECK(is_file("test.dat"), == 0);
}

static void stub_test_gui_timers()
{
    /* GUI timers */
    
    /* SetTimerAfter, CancelTimer */
    {
        int t0 = get_us_clock()/1000;
        int ta0 = 0;

        /* this one should overrun */
        timer_func = 0;
        TEST_FUNC_CHECK(SetTimerAfter(0, timer_cbr, overrun_cbr, 0), == 0x15);
        TEST_FUNC_CHECK(timer_func, == 2);
        ta0 = timer_arg;

        /* this one should not overrun */
        timer_func = 0;
        TEST_FUNC_CHECK(SetTimerAfter(1000, timer_cbr, overrun_cbr, 0), % 2 == 0);
        TEST_VOID(msleep(900));
        TEST_FUNC_CHECK(timer_func, == 0);  /* ta0 +  900 => CBR should not be called yet */
        TEST_VOID(msleep(200));
        TEST_FUNC_CHECK(timer_func, == 1);  /* ta0 + 1100 => CBR should be called by now */
        TEST_FUNC_CHECK(ABS((timer_time/1000 - t0) - 1000), <= 20);
        TEST_FUNC_CHECK(ABS((timer_arg - ta0) - 1000), <= 20);
        // current time: ta0+1100

        /* this one should not call the CBR, because we'll cancel it */
        timer_func = 0;
        int timer;
        TEST_FUNC_CHECK(timer = SetTimerAfter(1000, timer_cbr, overrun_cbr, 0), % 2 == 0);
        TEST_VOID(msleep(400));
        TEST_VOID(CancelTimer(timer));
        TEST_FUNC_CHECK(timer_func, == 0);  /* ta0 + 1500 => CBR should be not be called, and we'll cancel it early */
        TEST_VOID(msleep(1500));
        TEST_FUNC_CHECK(timer_func, == 0);  /* ta0 + 3000 => CBR should be not be called, because it was canceled */
    }
    
    /* microsecond timer wraps around at 1048576 */
    int DeltaT(int a, int b)
    {
        return MOD(a - b, 1048576);
    }

    /* SetHPTimerNextTick, SetHPTimerAfterTimeout, SetHPTimerAfterNow */
    {
        /* run these tests in PLAY mode, because the CPU usage is higher in other modes, and may influence the results */
        enter_play_mode();

        int64_t t0 = get_us_clock();
        int ta0 = 0;

        /* this one should overrun */
        timer_func = 0;
        TEST_FUNC_CHECK(SetHPTimerAfterNow(0, timer_cbr, overrun_cbr, 0), == 0x15);
        TEST_FUNC_CHECK(timer_func, == 2);
        ta0 = timer_arg;

        /* this one should not overrun */
        timer_func = 0;
        TEST_FUNC_CHECK(SetHPTimerAfterNow(100000, timer_cbr, overrun_cbr, 0), % 2 == 0);
        TEST_VOID(msleep(90));
        TEST_FUNC_CHECK(timer_func, == 0);  /* ta0 +  90000 => CBR should not be called yet */
        TEST_VOID(msleep(20));
        TEST_FUNC_CHECK(timer_func, == 1);  /* ta0 + 110000 => CBR should be called by now */
        
        TEST_FUNC_CHECK(ABS(DeltaT(timer_time, t0) - 100000), <= 2000);
        TEST_FUNC_CHECK(ABS(DeltaT(timer_arg, ta0) - 100000), <= 2000);
        TEST_FUNC_CHECK(ABS((get_us_clock() - t0) - 110000), <= 2000);

        /* this one should call SetHPTimerNextTick in the CBR */
        timer_func = 0;
        TEST_FUNC_CHECK(SetHPTimerAfterNow(90000, next_tick_cbr, overrun_cbr, 0), % 2 == 0);
        TEST_VOID(msleep(80));
        TEST_FUNC_CHECK(timer_func, == 0);  /* ta0 + 190000 => CBR should not be called yet */
        TEST_VOID(msleep(20));
        TEST_FUNC_CHECK(timer_func, == 3);  /* ta0 + 210000 => next_tick_cbr should be called by now */
                                                /* and it will setup timer_cbr via SetHPTimerNextTick */
        TEST_VOID(msleep(80));
        TEST_FUNC_CHECK(timer_func, == 3);  /* ta0 + 290000 => timer_cbr should not be called yet */
        TEST_VOID(msleep(20));
        TEST_FUNC_CHECK(timer_func, == 1);  /* ta0 + 310000 => timer_cbr should be called by now */
        TEST_FUNC_CHECK(ABS(DeltaT(timer_time, t0) - 300000), <= 2000);
        TEST_FUNC_CHECK(ABS(DeltaT(timer_arg, ta0) - 300000), <= 2000);
        TEST_FUNC_CHECK(ABS((get_us_clock() - t0) - 310000), <= 2000);
    }
}

static void stub_test_other_timers()
{
    // digic clock, msleep
    int t0, t1;
    TEST_FUNC(t0 = GET_DIGIC_TIMER());
    TEST_VOID(msleep(250));
    TEST_FUNC(t1 = GET_DIGIC_TIMER());
    TEST_FUNC_CHECK(ABS(MOD(t1-t0, 1048576)/1000 - 250), < 30);

    // calendar
    struct tm now;
    int s0, s1;
    TEST_VOID(LoadCalendarFromRTC( &now ));
    TEST_FUNC(s0 = now.tm_sec);

    TEST_MSG(
        "       Date/time: %04d/%02d/%02d %02d:%02d:%02d\n",
        now.tm_year + 1900,
        now.tm_mon + 1,
        now.tm_mday,
        now.tm_hour,
        now.tm_min,
        now.tm_sec
    );

    TEST_VOID(msleep(1500));
    TEST_VOID(LoadCalendarFromRTC( &now ));
    TEST_FUNC(s1 = now.tm_sec);
    TEST_FUNC_CHECK(MOD(s1-s0, 60), >= 1);
    TEST_FUNC_CHECK(MOD(s1-s0, 60), <= 2);
}

static void stub_test_malloc_n_allocmem()
{
    // mallocs
    // bypass the memory backend and use low-level calls only for these tests
    // run this test 200 times to check for memory leaks
    for (int i = 0; i < 200; i++)
    {
        int stub_silence = (i > 0);
        int m0, m1, m2;
        void* p;
        TEST_FUNC(m0 = MALLOC_FREE_MEMORY);
        TEST_FUNC_CHECK(p = (void*)_malloc(50*1024), != 0);
        TEST_FUNC_CHECK(CACHEABLE(p), == (int)p);
        TEST_FUNC(m1 = MALLOC_FREE_MEMORY);
        TEST_VOID(_free(p));
        TEST_FUNC(m2 = MALLOC_FREE_MEMORY);
        TEST_FUNC_CHECK(ABS((m0-m1) - 50*1024), < 2048);
        TEST_FUNC_CHECK(ABS(m0-m2), < 2048);

        TEST_FUNC(m0 = GetFreeMemForAllocateMemory());
        TEST_FUNC_CHECK(p = (void*)_AllocateMemory(128*1024), != 0);
        TEST_FUNC_CHECK(CACHEABLE(p), == (int)p);
        TEST_FUNC(m1 = GetFreeMemForAllocateMemory());
        TEST_VOID(_FreeMemory(p));
        TEST_FUNC(m2 = GetFreeMemForAllocateMemory());
        TEST_FUNC_CHECK(ABS((m0-m1) - 128*1024), < 2048);
        TEST_FUNC_CHECK(ABS(m0-m2), < 2048);

        // these buffers may be from different memory pools, just check for leaks in main pools
        int m01, m02, m11, m12;
        TEST_FUNC(m01 = MALLOC_FREE_MEMORY);
        TEST_FUNC(m02 = GetFreeMemForAllocateMemory());
        TEST_FUNC_CHECK(p = (void*)_alloc_dma_memory(128*1024), != 0);
        TEST_FUNC_CHECK(UNCACHEABLE(p), == (int)p);
        TEST_FUNC_CHECK(CACHEABLE(p), != (int)p);
        TEST_FUNC_CHECK(UNCACHEABLE(CACHEABLE(p)), == (int)p);
        TEST_VOID(_free_dma_memory(p));
        TEST_FUNC_CHECK(p = (void*)_shoot_malloc(16*1024*1024), != 0);
        TEST_FUNC_CHECK(UNCACHEABLE(p), == (int)p);
        TEST_VOID(_shoot_free(p));
        TEST_FUNC(m11 = MALLOC_FREE_MEMORY);
        TEST_FUNC(m12 = GetFreeMemForAllocateMemory());
        TEST_FUNC_CHECK(ABS(m01-m11), < 2048);
        TEST_FUNC_CHECK(ABS(m02-m12), < 2048);
    }
}

static void stub_test_exmem()
{
    // exmem
    // run this test 20 times to check for memory leaks
    for (int i = 0; i < 20; i++)
    {
        int stub_silence = (i > 0);

        struct memSuite * suite = 0;
        struct memChunk * chunk = 0;
        void* p = 0;
        int total = 0;

        // contiguous allocation
        // assume we can allocate at least 16MB continuously
        TEST_FUNC_CHECK(suite = shoot_malloc_suite_contig(16*1024*1024), != 0);
        TEST_FUNC_CHECK_STR(suite->signature, "MemSuite");
        TEST_FUNC_CHECK(suite->num_chunks, == 1);
        TEST_FUNC_CHECK(suite->size, == 16*1024*1024);
        TEST_FUNC_CHECK(chunk = GetFirstChunkFromSuite(suite), != 0);
        TEST_FUNC_CHECK_STR(chunk->signature, "MemChunk");
        TEST_FUNC_CHECK(chunk->size, == 16*1024*1024);
        TEST_FUNC_CHECK(p = GetMemoryAddressOfMemoryChunk(chunk), != 0);
        TEST_FUNC_CHECK(UNCACHEABLE(p), == (int)p);
        TEST_VOID(shoot_free_suite(suite); suite = 0; chunk = 0;);

        // contiguous allocation, largest block
        int largest_shoot_block = 0;
        TEST_FUNC_CHECK(suite = shoot_malloc_suite_contig(0), != 0);
        TEST_FUNC_CHECK_STR(suite->signature, "MemSuite");
        TEST_FUNC_CHECK(suite->num_chunks, == 1);
        TEST_FUNC_CHECK(suite->size, > 16*1024*1024);
        TEST_FUNC_CHECK(chunk = GetFirstChunkFromSuite(suite), != 0);
        TEST_FUNC_CHECK_STR(chunk->signature, "MemChunk");
        TEST_FUNC_CHECK(chunk->size, == suite->size);
        TEST_FUNC_CHECK(p = GetMemoryAddressOfMemoryChunk(chunk), != 0);
        TEST_FUNC_CHECK(UNCACHEABLE(p), == (int)p);
        TEST_FUNC(largest_shoot_block = suite->size);
        TEST_MSG("[INFO] largest_shoot_block: %s\n", format_memory_size(largest_shoot_block));
        TEST_VOID(shoot_free_suite(suite); suite = 0; chunk = 0;);

        // fragmented allocation
        TEST_FUNC_CHECK(suite = shoot_malloc_suite(largest_shoot_block + 1024*1024), != 0);
        TEST_FUNC_CHECK_STR(suite->signature, "MemSuite");
        TEST_FUNC_CHECK(suite->num_chunks, > 1);
        TEST_FUNC_CHECK(suite->size, == largest_shoot_block + 1024*1024);

        // iterating through chunks
        total = 0;
        TEST_FUNC_CHECK(chunk = GetFirstChunkFromSuite(suite), != 0);
        while(chunk)
        {
            TEST_FUNC_CHECK_STR(chunk->signature, "MemChunk");
            TEST_FUNC_CHECK(total += chunk->size, <= largest_shoot_block + 1024*1024);
            TEST_FUNC_CHECK(p = GetMemoryAddressOfMemoryChunk(chunk), != 0);
            TEST_FUNC_CHECK(UNCACHEABLE(p), == (int)p);
            TEST_FUNC(chunk = GetNextMemoryChunk(suite, chunk));
        }
        TEST_FUNC_CHECK(total, == largest_shoot_block + 1024*1024);
        TEST_VOID(shoot_free_suite(suite); suite = 0; chunk = 0; );

        // fragmented allocation, max size
        TEST_FUNC_CHECK(suite = shoot_malloc_suite(0), != 0);
        TEST_FUNC_CHECK_STR(suite->signature, "MemSuite");
        TEST_FUNC_CHECK(suite->num_chunks, > 1);
        TEST_FUNC_CHECK(suite->size, >= largest_shoot_block + 1024*1024);

        // iterating through chunks
        total = 0;
        TEST_FUNC_CHECK(chunk = GetFirstChunkFromSuite(suite), != 0);
        while(chunk)
        {
            TEST_FUNC_CHECK_STR(chunk->signature, "MemChunk");
            TEST_FUNC_CHECK(total += chunk->size, <= suite->size);
            TEST_FUNC_CHECK(p = GetMemoryAddressOfMemoryChunk(chunk), != 0);
            TEST_FUNC_CHECK(UNCACHEABLE(p), == (int)p);
            TEST_FUNC(chunk = GetNextMemoryChunk(suite, chunk));
        }
        TEST_FUNC_CHECK(total, == suite->size);
        TEST_VOID(shoot_free_suite(suite); suite = 0; chunk = 0; );
    }
}

static void stub_test_strings()
{
    // strlen
    TEST_FUNC_CHECK(strlen("abc"), == 3);
    TEST_FUNC_CHECK(strlen("qwertyuiop"), == 10);
    TEST_FUNC_CHECK(strlen(""), == 0);

    // strcpy
    char msg[10];
    TEST_FUNC_CHECK(strcpy(msg, "hi there"), == (int)msg);
    TEST_FUNC_CHECK_STR(msg, "hi there");

    // strcmp, snprintf
    // gcc will optimize strcmp calls with constant arguments, so use snprintf to force gcc to call strcmp
    char a[50]; char b[50];

    TEST_FUNC_CHECK(snprintf(a, sizeof(a), "foo"), == 3);
    TEST_FUNC_CHECK(snprintf(b, sizeof(b), "foo"), == 3);
    TEST_FUNC_CHECK(strcmp(a, b), == 0);

    TEST_FUNC_CHECK(snprintf(a, sizeof(a), "bar"), == 3);
    TEST_FUNC_CHECK(snprintf(b, sizeof(b), "baz"), == 3);
    TEST_FUNC_CHECK(strcmp(a, b), < 0);

    TEST_FUNC_CHECK(snprintf(a, sizeof(a), "Display"), == 7);
    TEST_FUNC_CHECK(snprintf(b, sizeof(b), "Defishing"), == 9);
    TEST_FUNC_CHECK(strcmp(a, b), > 0);

    // vsnprintf (called by snprintf)
    char buf[4];
    TEST_FUNC_CHECK(snprintf(buf, 3, "%d", 1234), == 2);
    TEST_FUNC_CHECK_STR(buf, "12");

    // memcpy, memset, bzero32
    char foo[] __attribute__((aligned(32))) = "qwertyuiop";
    char bar[] __attribute__((aligned(32))) = "asdfghjkl;";
    TEST_FUNC_CHECK(memcpy(foo, bar, 6), == (int)foo);
    TEST_FUNC_CHECK_STR(foo, "asdfghuiop");
    TEST_FUNC_CHECK(memset(bar, '*', 5), == (int)bar);
    TEST_FUNC_CHECK_STR(bar, "*****hjkl;");
    TEST_VOID(bzero32(bar + 5, 5));
    TEST_FUNC_CHECK_STR(bar, "****");
}

static void stub_test_engio()
{
    // engio
    TEST_VOID(EngDrvOut(LCD_Palette[0], 0x1234));
    TEST_FUNC_CHECK(shamem_read(LCD_Palette[0]), == 0x1234);
}

static void stub_test_display()
{
    // call, DISPLAY_IS_ON
    TEST_VOID(call("TurnOnDisplay"));
    TEST_FUNC_CHECK(DISPLAY_IS_ON, != 0);
    TEST_VOID(call("TurnOffDisplay"));
    TEST_FUNC_CHECK(DISPLAY_IS_ON, == 0);
    TEST_VOID(call("TurnOnDisplay"));
    TEST_FUNC_CHECK(DISPLAY_IS_ON, != 0);
}

static void stub_test_gui()
{
    // SetGUIRequestMode, CURRENT_GUI_MODE
    TEST_VOID(SetGUIRequestMode(1); msleep(1000););
    TEST_FUNC_CHECK(CURRENT_GUI_MODE, == 1);
    TEST_VOID(SetGUIRequestMode(2); msleep(1000););
    TEST_FUNC_CHECK(CURRENT_GUI_MODE, == 2);
    TEST_VOID(SetGUIRequestMode(0); msleep(1000););
    TEST_FUNC_CHECK(CURRENT_GUI_MODE, == 0);
    TEST_FUNC_CHECK(display_idle(), != 0);

    // GUI_Control
    msleep(1000);
    TEST_VOID(GUI_Control(BGMT_PLAY, 0, 0, 0); msleep(1000););
    TEST_FUNC_CHECK(PLAY_MODE, != 0);
    TEST_FUNC_CHECK(MENU_MODE, == 0);
    TEST_VOID(GUI_Control(BGMT_MENU, 0, 0, 0); msleep(1000););
    TEST_FUNC_CHECK(MENU_MODE, != 0);
    TEST_FUNC_CHECK(PLAY_MODE, == 0);

    // also check dialog signature here, because display is on for sure
    struct gui_task * current = gui_task_list.current;
    struct dialog * dialog = current->priv;
    TEST_FUNC_CHECK_STR(dialog->type, "DIALOG");

    TEST_VOID(GUI_Control(BGMT_MENU, 0, 0, 0); msleep(500););
    TEST_FUNC_CHECK(MENU_MODE, == 0);
    TEST_FUNC_CHECK(PLAY_MODE, == 0);

    // sw1
    TEST_VOID(SW1(1,100));
    TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 1);
    TEST_VOID(SW1(0,100));
    TEST_FUNC_CHECK(HALFSHUTTER_PRESSED, == 0);
    
    /* take a picture and go to play mode */
    lens_take_picture(64, AF_DISABLE);
    msleep(2000);
    enter_play_mode();
    TEST_FUNC_CHECK(is_play_mode(), != 0);
    TEST_FUNC_CHECK(is_pure_play_photo_mode(), != 0);
    TEST_FUNC_CHECK(is_pure_play_movie_mode(), == 0);
    
    /* try to erase the picture (don't actually erase it; just check dialog codes) */
    fake_simple_button(BGMT_TRASH);
    msleep(500);
    TEST_FUNC_CHECK(is_play_mode(), != 0);
    TEST_FUNC_CHECK(is_pure_play_photo_mode(), == 0);
    TEST_FUNC_CHECK(is_pure_play_movie_mode(), == 0);
    fake_simple_button(BGMT_TRASH);
    msleep(500);

    /* record a movie and go to play mode */
    movie_start();
    msleep(2000);
    movie_end();
    msleep(2000);
    enter_play_mode();
    TEST_FUNC_CHECK(is_play_mode(), != 0);
    TEST_FUNC_CHECK(is_pure_play_photo_mode(), == 0);
    TEST_FUNC_CHECK(is_pure_play_movie_mode(), != 0);

    /* try to erase the movie (don't actually erase it; just check dialog codes) */
    fake_simple_button(BGMT_TRASH);
    msleep(500);
    TEST_FUNC_CHECK(is_play_mode(), != 0);
    TEST_FUNC_CHECK(is_pure_play_photo_mode(), == 0);
    TEST_FUNC_CHECK(is_pure_play_movie_mode(), == 0);
    fake_simple_button(BGMT_TRASH);
    msleep(500);
}

static int test_task_created = 0;
static void test_task() { test_task_created = 1; }

static void stub_test_dryos()
{
    // task_create
    TEST_FUNC(task_create("test", 0x1c, 0x1000, test_task, 0));
    msleep(100);
    TEST_FUNC_CHECK(test_task_created, == 1);
    TEST_FUNC_CHECK_STR(get_current_task_name(), "run_test");
    TEST_FUNC_CHECK_STR(get_task_name_from_id(current_task->taskId), "run_test");

    extern int task_max;
    TEST_FUNC_CHECK(task_max, >= 104);    /* so far, task_max is 104 on most cameras */
    TEST_FUNC_CHECK(task_max, <= 512);    /* I guess it's not higher than that */

    // mq
    static struct msg_queue * mq = 0;
    int m = 0;
    TEST_FUNC_CHECK(mq = mq ? mq : (void*)msg_queue_create("test", 5), != 0);
    TEST_FUNC_CHECK(msg_queue_post(mq, 0x1234567), == 0);
    TEST_FUNC_CHECK(msg_queue_receive(mq, (struct event **) &m, 500), == 0);
    TEST_FUNC_CHECK(m, == 0x1234567);
    TEST_FUNC_CHECK(msg_queue_receive(mq, (struct event **) &m, 500), != 0);

    // sem
    static struct semaphore * sem = 0;
    TEST_FUNC_CHECK(sem = sem ? sem : create_named_semaphore("test", 1), != 0);
    TEST_FUNC_CHECK(take_semaphore(sem, 500), == 0);
    TEST_FUNC_CHECK(take_semaphore(sem, 500), != 0);
    TEST_FUNC_CHECK(give_semaphore(sem), == 0);
    TEST_FUNC_CHECK(take_semaphore(sem, 500), == 0);
    TEST_FUNC_CHECK(give_semaphore(sem), == 0);

    // recursive lock
    static void * rlock = 0;
    TEST_FUNC_CHECK(rlock = rlock ? rlock : CreateRecursiveLock(0), != 0);
    TEST_FUNC_CHECK(AcquireRecursiveLock(rlock, 500), == 0);
    TEST_FUNC_CHECK(AcquireRecursiveLock(rlock, 500), == 0);
    TEST_FUNC_CHECK(ReleaseRecursiveLock(rlock), == 0);
    TEST_FUNC_CHECK(ReleaseRecursiveLock(rlock), == 0);
    TEST_FUNC_CHECK(ReleaseRecursiveLock(rlock), != 0);
}

static void stub_test_model_id()
{
    // model, firmware version
    TEST_MSG("[INFO] Camera model: %s %s (0x%X %s)\n", camera_model, firmware_version, camera_model_id, __camera_model_short);

    TEST_FUNC_CHECK(is_camera("DIGIC", "*"), == 1);
    TEST_FUNC_CHECK(is_camera(__camera_model_short, firmware_version), == 1);

    if (is_camera("5D3", "*"))
    {
        TEST_FUNC_CHECK(is_camera("5D2", "*"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "4"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "5"), == 1);
    }
    else if (is_camera("60D", "*"))
    {
        TEST_FUNC_CHECK(is_camera("600D", "*"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "5"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "4"), == 1);
    }
    else if (is_camera("80D", "*"))
    {
        TEST_FUNC_CHECK(is_camera("70D", "*"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "5"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "6"), == 1);
    }
    else if (is_camera("200D", "*"))
    {
        TEST_FUNC_CHECK(is_camera("100D", "*"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "6"), == 0);
        TEST_FUNC_CHECK(is_camera("DIGIC", "7"), == 1);
    }
}

static void stub_test_save_log()
{
    FILE* log = FIO_CreateFile("ML/LOGS/stubtest.log");
    if (log)
    {
        FIO_WriteFile(log, stub_log_buf, stub_log_len);
        FIO_CloseFile(log);
    }
}

static void stub_test_task(void* arg)
{
    if (stub_log_buf) return;
    stub_log_buf = fio_malloc(stub_max_log_len);
    if (!stub_log_buf) return;

    msleep(1000);
    console_show();

    stub_passed_tests = 0;
    stub_failed_tests = 0;
    
    enter_play_mode();
    TEST_FUNC_CHECK(is_play_mode(), != 0);

    // this test can be repeated many times, as burn-in test
    int n = (int)arg > 0 ? 1 : 100;
    msleep(1000);
    info_led_on();

    /* save log after each sub-test */
    for (int i=0; i < n; i++)
    {
        stub_test_model_id();               stub_test_save_log();
        stub_test_edmac();                  stub_test_save_log();
        stub_test_cache();                  stub_test_save_log();
        stub_test_af();                     stub_test_save_log();
        stub_test_file_io();                stub_test_save_log();
        stub_test_gui_timers();             stub_test_save_log();
        stub_test_other_timers();           stub_test_save_log();
        stub_test_malloc_n_allocmem();      stub_test_save_log();
        stub_test_exmem();                  stub_test_save_log();
        stub_test_strings();                stub_test_save_log();
        stub_test_engio();                  stub_test_save_log();
        stub_test_display();                stub_test_save_log();
        stub_test_dryos();                  stub_test_save_log();
        stub_test_gui();                    stub_test_save_log();

        beep();
    }

    enter_play_mode();

    TEST_MSG(
        "=========================================================\n"
        "Test complete, %d passed, %d failed.\n.",
        stub_passed_tests, stub_failed_tests
    );

    stub_test_save_log();
    fio_free(stub_log_buf);
    stub_log_buf = 0;
}

static void rpc_test_task(void* unused)
{
    uint32_t loops = 0;

    ml_rpc_verbose(1);
    while(1)
    {
        msleep(50);

        ml_rpc_send(ML_RPC_PING, GET_DIGIC_TIMER(), 0, 0, 1);
        loops++;
    }
    ml_rpc_verbose(0);
}

static void stress_test_task(void* unused)
{
    NotifyBox(10000, "Stability Test..."); msleep(2000);

    msleep(2000);

    /* 50D: taking pics while REC crashes with Canon firmware too */
    if (!is_camera("50D", "*"))
    {
        ensure_movie_mode();
        msleep(1000);
        for (int i = 0; i <= 5; i++)
        {
            NotifyBox(1000, "Pics while recording: %d", i);
            movie_start();
            msleep(1000);
            lens_take_picture(64, 0);
            msleep(1000);
            lens_take_picture(64, 0);
            msleep(1000);
            lens_take_picture(64, 0);
            while (lens_info.job_state) msleep(100);
            while (!lv) msleep(100);
            msleep(1000);
            movie_end();
            msleep(2000);
        }
    }

    msleep(2000);

    extern struct semaphore * gui_sem;

    msleep(2000);

    for (int i = 0; i <= 1000; i++)
    {
        NotifyBox(1000, "ML menu toggle: %d", i);

        if (i == 250)
        {
            msleep(2000);
            gui_stop_menu();
            msleep(500);
            if (!lv) force_liveview();
        }

        if (i == 500)
        {
            msleep(2000);
            gui_stop_menu();
            msleep(500);
            ensure_movie_mode();
            movie_start();
        }

        if (i == 750)
        {
            msleep(2000);
            gui_stop_menu();
            msleep(500);
            movie_end();
            msleep(2000);
            fake_simple_button(BGMT_PLAY);
            msleep(1000);
        }

        give_semaphore(gui_sem);
        msleep(rand()%100);
        info_led_blink(1,50,50);

    }
    msleep(2000);
    gui_stop_menu();
    msleep(1000);
    if (!lv) force_liveview();
    msleep(2000);

    NotifyBox(1000, "Cropmarks preview...");
    select_menu_by_name("Overlay", "Cropmarks");
    give_semaphore( gui_sem );
    msleep(500);
    menu_open_submenu();
    msleep(100);
    for (int i = 0; i <= 100; i++)
    {
        fake_simple_button(BGMT_WHEEL_RIGHT);
        msleep(rand()%500);
    }
    gui_stop_menu();
    msleep(2000);

    NotifyBox(1000, "ML menu scroll...");
    give_semaphore(gui_sem);
    msleep(1000);
    for (int i = 0; i <= 5000; i++)
    {
        static int dir = 0;
        switch(dir)
        {
            case 0: fake_simple_button(BGMT_WHEEL_LEFT); break;
            case 1: fake_simple_button(BGMT_WHEEL_RIGHT); break;
            case 2: fake_simple_button(BGMT_WHEEL_UP); break;
            case 3: fake_simple_button(BGMT_WHEEL_DOWN); break;
            case 4: fake_simple_button(BGMT_INFO); break;
            case 5: fake_simple_button(BGMT_MENU); break;
            //~ case 6: fake_simple_button(BGMT_PRESS_ZOOM_IN); break;
        }
        dir = MOD(dir + rand()%3 - 1, 7);
        msleep(20);
    }
    gui_stop_menu();

    msleep(2000);

    beep();
    fake_simple_button(BGMT_PLAY); msleep(1000);
    for (int i = 0; i < 100; i++)
    {
        NotifyBox(1000, "PLAY: image compare: %d", i);
        playback_compare_images_task(1);
    }
    exit_play_qr_mode();
    msleep(2000);

    fake_simple_button(BGMT_PLAY); msleep(1000);
    for (int i = 0; i < 10; i++)
    {
        NotifyBox(1000, "PLAY: exposure fusion: %d", i);
        expfuse_preview_update_task(1);
    }
    exit_play_qr_mode();
    msleep(2000);

    fake_simple_button(BGMT_PLAY); msleep(1000);
    for (int i = 0; i < 50; i++)
    {
        NotifyBox(1000, "PLAY scrolling: %d", i);
        next_image_in_play_mode(1);
    }
    extern int timelapse_playback;
    timelapse_playback = 1;
    for (int i = 0; i < 50; i++)
    {
        NotifyBox(1000, "PLAY scrolling: %d", i+50);
        msleep(200);
    }
    timelapse_playback = 0;
    exit_play_qr_mode();

    msleep(2000);

    if (!lv) force_liveview();

    for (int i = 0; i <= 100; i++)
    {
        int r = rand()%3;
        set_lv_zoom(r == 0 ? 1 : r == 1 ? 5 : 10);
        NotifyBox(1000, "LV zoom test: %d", i);
        msleep(rand()%200);
    }
    set_lv_zoom(1);
    msleep(2000);

    for (int i = 0; i <= 100; i++)
    {
        set_expsim(i%3);
        NotifyBox(1000, "ExpSim toggle: %d", i/10);
        msleep(rand()%100);
    }

    msleep(2000);

    for (int i = 0; i <= 100; i++)
    {
        bv_toggle(0, 1);
        NotifyBox(1000, "Exp.Override toggle: %d", i/10);
        msleep(rand()%100);
    }
    msleep(2000);

/*    for (int i = 0; i < 100; i++)
    {
        NotifyBox(1000, "Disabling Canon GUI (%d)...", i);
        canon_gui_disable();
        msleep(rand()%300);
        canon_gui_enable();
        msleep(rand()%300);
    } */

    msleep(2000);

    NotifyBox(10000, "LCD backlight...");
    int old_backlight_level = backlight_level;
    for (int i = 0; i < 5; i++)
    {
        for (int k = 1; k <= 7; k++)
        {
            set_backlight_level(k);
            msleep(50);
        }
        for (int k = 7; k >= 1; k--)
        {
            set_backlight_level(k);
            msleep(50);
        }
    }
    set_backlight_level(old_backlight_level);

    if (!lv) force_liveview();
    for (int k = 0; k < 10; k++)
    {
        NotifyBox(1000, "LiveView / Playback (%d)...", k*10);
        fake_simple_button(BGMT_PLAY);
        msleep(rand() % 1000);
        SW1(1, rand()%100);
        SW1(0, rand()%100);
        msleep(rand() % 1000);
    }
    if (!lv) force_liveview();
    msleep(2000);
    lens_set_rawiso(0);
    for (int k = 0; k < 5; k++)
    {
        NotifyBox(1000, "LiveView gain test: %d", k*20);
        for (int i = 0; i <= 16; i++)
        {
            set_display_gain_equiv(1<<i);
            msleep(100);
        }
        for (int i = 16; i >= 0; i--)
        {
            set_display_gain_equiv(1<<i);
            msleep(100);
        }
    }
    set_display_gain_equiv(0);

    msleep(1000);

    for (int i = 0; i <= 10; i++)
    {
        NotifyBox(1000, "LED blinking: %d", i*10);
        info_led_blink(10, i*3, (10-i)*3);
    }

    msleep(2000);

    for (int i = 0; i <= 100; i++)
    {
        NotifyBox(1000, "Redraw test: %d", i);
        msleep(50);
        redraw();
        msleep(50);
    }

    msleep(2000);

    NotifyBox(10000, "Menu scrolling");
    fake_simple_button(BGMT_MENU);
    msleep(1000);
    for (int i = 0; i < 5000; i++)
        fake_simple_button(BGMT_WHEEL_LEFT);
    for (int i = 0; i < 5000; i++)
        fake_simple_button(BGMT_WHEEL_RIGHT);
    SW1(1,0);
    SW1(0,0);

    stress_test_picture(2, 2000); // make sure we have at least 2 pictures for scrolling :)

    msleep(2000);

#if 0 // unsafe
    for (int i = 0; i <= 10; i++)
    {
        NotifyBox(1000, "Mode switching: %d", i*10);
        set_shooting_mode(SHOOTMODE_AUTO);    msleep(100);
        set_shooting_mode(SHOOTMODE_MOVIE);    msleep(2000);
        set_shooting_mode(SHOOTMODE_SPORTS);    msleep(100);
        set_shooting_mode(SHOOTMODE_NIGHT);    msleep(100);
        set_shooting_mode(SHOOTMODE_CA);    msleep(100);
        set_shooting_mode(SHOOTMODE_M);    msleep(100);
        ensure_bulb_mode(); msleep(100);
        set_shooting_mode(SHOOTMODE_TV);    msleep(100);
        set_shooting_mode(SHOOTMODE_AV);    msleep(100);
        set_shooting_mode(SHOOTMODE_P);    msleep(100);
    }

    stress_test_picture(2, 2000);
#endif

    if (!lv) force_liveview();
    NotifyBox(10000, "Focus tests...");
    msleep(2000);
    for (int i = 1; i <= 3; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            lens_focus( 1, i, 1, 0);
            lens_focus(-1, i, 1, 0);
        }
    }

    msleep(2000);

    NotifyBox(10000, "Expo tests...");

    if (!lv) force_liveview();
    msleep(1000);
    for (int i = KELVIN_MIN; i <= KELVIN_MAX; i += KELVIN_STEP)
    {
        NotifyBox(1000, "Kelvin: %d", i);
        lens_set_kelvin(i); msleep(200);
    }
    lens_set_kelvin(6500);

    stress_test_picture(2, 2000);

    set_shooting_mode(SHOOTMODE_M);
    msleep(1000);
    if (!lv) force_liveview();
    msleep(1000);

    for (int i = 72; i <= 136; i++)
    {
        NotifyBox(1000, "ISO: raw %d  ", i);
        lens_set_rawiso(i); msleep(200);
    }
    lens_set_rawiso(ISO_400);

    stress_test_picture(2, 2000);

    msleep(5000);
    if (!lv) force_liveview();
    msleep(1000);

    for (int i = 0; i <= 100; i++)
    {
        NotifyBox(1000, "Pause LiveView: %d", i);
        PauseLiveView(); msleep(rand()%200);
        ResumeLiveView(); msleep(rand()%200);
    }

    stress_test_picture(2, 2000);

    msleep(2000);
    if (!lv) force_liveview();
    msleep(1000);

    for (int i = 0; i <= 100; i++)
    {
        NotifyBox(1000, "BMP overlay: %d", i);
        bmp_off(); msleep(rand()%200);
        bmp_on(); msleep(rand()%200);
    }

    stress_test_picture(2, 2000);

    msleep(2000);
    if (!lv) force_liveview();
    msleep(1000);

    for (int i = 0; i <= 100; i++)
    {
        NotifyBox(1000, "Display on/off: %d", i);
        display_off(); msleep(rand()%200);
        display_on(); msleep(rand()%200);
    }

    msleep(3000); // 60D: display on/off is slow and will continue a while after this

    ensure_photo_mode();

    stress_test_picture(2, 2000);

    NotifyBox(10000, "LiveView switch...");
    set_shooting_mode(SHOOTMODE_M);
    for (int i = 0; i < 21; i++)
    {
        fake_simple_button(BGMT_LV); msleep(rand()%200);
    }

    stress_test_picture(2, 2000);

    set_shooting_mode(SHOOTMODE_BULB);

    msleep(1000);
    NotifyBox(10000, "Bulb picture taking");
    bulb_take_pic(2000);
    bulb_take_pic(100);
    bulb_take_pic(1500);
    bulb_take_pic(10);
    bulb_take_pic(1000);
    bulb_take_pic(1);

    NotifyBox(10000, "Movie recording");
    ensure_movie_mode();
    msleep(1000);
    for (int i = 0; i <= 5; i++)
    {
        NotifyBox(10000, "Movie recording: %d", i);
        movie_start();
        msleep(5000);
        movie_end();
        msleep(5000);
    }

    stress_test_picture(2, 2000);

    NotifyBox(2000, "Test complete."); msleep(2000);
    NotifyBox(2000, "Is the camera still working?"); msleep(2000);
    NotifyBox(10000, ":)");
    //~ NotifyBox(10000, "Burn-in test (will take hours!)");
    //~ set_shooting_mode(SHOOTMODE_M);
    //~ xx_test2(0);

}

/*
static void stress_test_toggle_menu_item(char* menu_name, char* item_name)
{
    extern struct semaphore * gui_sem;
    select_menu_by_name(menu_name, item_name);
    if (!gui_menu_shown()) give_semaphore( gui_sem );
    msleep(400);
    fake_simple_button(BGMT_PRESS_SET);
    msleep(200);
    give_semaphore( gui_sem );
    msleep(200);
    return;
} */

static void stress_test_toggle_random_menu_item()
{
    extern struct semaphore * gui_sem;
    if (!gui_menu_shown()) give_semaphore( gui_sem );
    msleep(400);
    int dx = rand() % 20 - 10;
    int dy = rand() % 20 - 10;
    for (int i = 0; i < ABS(dx); i++)
        fake_simple_button(dx > 0 ? BGMT_WHEEL_RIGHT : BGMT_WHEEL_LEFT);
    msleep(200);
    for (int i = 0; i < ABS(dy); i++)
        fake_simple_button(dy > 0 ? BGMT_WHEEL_UP : BGMT_WHEEL_DOWN);
    msleep(200);
    fake_simple_button(BGMT_PRESS_SET);
    msleep(200);
    give_semaphore( gui_sem );
    msleep(200);
    return;
}

static void stress_test_random_action()
{
    switch (rand() % 50)
    {
        case 0:
            lens_take_picture(64, rand() % 2);
            return;
        case 1:
            fake_simple_button(BGMT_LV);
            return;
        case 2:
            fake_simple_button(BGMT_PLAY);
            return;
        case 3:
            fake_simple_button(BGMT_MENU);
            return;
        default:
            stress_test_toggle_random_menu_item();
    }
}

static void stress_test_random_task(void* unused)
{
    extern int config_autosave;
    config_autosave = 0; // this will make many changes in menu, don't save them
    TASK_LOOP
    {
        stress_test_random_action();
        //~ stress_test_toggle_menu_item("Play", "Zoom in PLAY mode");
        msleep(rand() % 1000);
    }
}

/*static void stress_test_random_action_simple()
{
    {
        switch (rand() % 4)
        {
            case 0:
            {
                stress_test_toggle_menu_item("Overlay", "Global Draw");
                return;
            }
            case 1:
                fake_simple_button(BGMT_PLAY);
                return;
            case 2:
                fake_simple_button(BGMT_MENU);
                return;
            case 3:
                fake_simple_button(BGMT_INFO);
                return;
        }
    }
}
*/

static void stress_test_menu_dlg_api_task(void* unused)
{
    msleep(2000);
    info_led_blink(5,50,50);
    extern struct semaphore * gui_sem;
    TASK_LOOP
    {
        give_semaphore(gui_sem);
        msleep(20);
    }
}

static void excessive_redraws_task()
{
    info_led_blink(5,50,1000);
    while(1)
    {
        if (gui_menu_shown()) menu_redraw();
        else redraw();
        msleep(10);
    }
}

static void bmp_fill_test_task()
{
    msleep(2000);
    while(1)
    {
        int x1 = rand() % 720;
        int x2 = rand() % 720;
        int y1 = rand() % 480;
        int y2 = rand() % 480;
        int xm = MIN(x1,x2); int xM = MAX(x1,x2);
        int ym = MIN(y1,y2); int yM = MAX(y1,y2);
        int w = xM-xm;
        int h = yM-ym;
        int c = rand() % 255;
        bmp_fill(c, xm, ym, w, h);
        msleep(20);
    }
}

#if 0
static void menu_duplicate_test()
{
    struct menu * menu_get_root();
    struct menu * menu = menu_get_root();
    
    for( ; menu ; menu = menu->next )
    {
        if (menu == my_menu) continue;
        if (menu == mod_menu) continue;
        
        struct menu_entry * entry = menu->children;
        for( ; entry ; entry = entry->next )
        {
            if (!entry->name) continue;
            if (entry->shidden) continue;
            
            struct menu_entry * e = entry_find_by_name(0, entry->name);
            
            if (e != entry)
            {
                printf("Duplicate: %s->%s\n", menu->name, entry->name);
            }
        }
    }
}

// for menu entries with custom toggle: check if it wraps around in both directions
static int entry_check_wrap(const char* name, const char* entry_name, int dir)
{
    struct menu_entry * entry = entry_find_by_name(name, entry_name);
    ASSERT(entry);
    ASSERT(entry->select);

    // we will need exclusive access to menu_display_info
    take_semaphore(menu_sem, 0);
    
    // if it doesn't seem to cycle, cancel earlier
    char first[MENU_MAX_VALUE_LEN];
    char last[MENU_MAX_VALUE_LEN];
    snprintf(first, sizeof(first), "%s", menu_get_str_value_from_script(name, entry_name));
    snprintf(last, sizeof(last), "%s", menu_get_str_value_from_script(name, entry_name));
    
    if (entry->icon_type == IT_ACTION)
        goto ok; // don't check actions
    
    if (strlen(first)==0)
        goto ok; // no value field, skip it
    
    for (int i = 0; i < 500; i++) // cycle until it returns to initial value
    {
        bmp_printf(FONT_MED, 0, 0, "%s->%s: %s (%s)                  ", name, entry_name, last, dir > 0 ? "+" : "-");

        // next value
        entry->select( entry->priv, dir);
        msleep(20); // we may need to wait for property handlers to update

        char* current = menu_get_str_value_from_script(name, entry_name);
        
        if (streq(current, last)) // value not changing? not good
        {
            printf("Value not changing: %s, %s -> %s (%s).\n", current, name, entry_name, dir > 0 ? "+" : "-");
            goto err;
        }
        
        if (streq(current, first)) // back to first value? success!
            goto ok;

        snprintf(last, sizeof(last), "%s", current);
    }
    printf("'Infinite' range: %s -> %s (%s)\n", name, entry_name, dir > 0 ? "+" : "-");

err:
    give_semaphore(menu_sem);
    return 0; // boo :(

ok:
    give_semaphore(menu_sem);
    return 1; // :)
}

void menu_check_wrap()
{
    int ok = 0;
    int bad = 0;
    struct menu * menu = menus;
    for( ; menu ; menu = menu->next )
    {
        struct menu_entry * entry = menu->children;
        for( ; entry ; entry = entry->next )
        {
            if (entry->shidden) continue;
            if (!entry->select) continue;
            
            int r = entry_check_wrap(menu->name, entry->name, 1);
            if (r) ok++; else bad++;

            r = entry_check_wrap(menu->name, entry->name, -1);
            if (r) ok++; else bad++;
            
            msleep(100);
        }
    }
    printf("Wrap test: %d OK, %d bad\n", ok, bad);
}

void menu_self_test()
{
    msleep(2000);
    console_show();
    menu_duplicate_test();
    printf("\n");
    menu_check_wrap();
}
#endif

static void srm_test_task()
{
    printf("SRM memory test...\n");
    msleep(2000);
    console_show();
    msleep(1000);
    
    /* let's see how much RAM we can get */
    struct memSuite * suite = srm_malloc_suite(0);
    struct memChunk * chunk = GetFirstChunkFromSuite(suite);
    printf("hSuite %x (%dx%s)\n", suite, suite->num_chunks, format_memory_size(chunk->size));
    
    printf("You should not be able to take pictures,\n");
    printf("but autofocus should work.\n");

    info_led_on();
    for (int i = 10; i >= 0; i--)
    {
        msleep(1000);
        printf("%d...", i);
    }
    printf("\b\b\n");
    info_led_off();
    
    srm_free_suite(suite);
    msleep(1000);

    printf("Now try taking some pictures during the test.\n");
    printf("It should work, and it should not crash.\n");
    msleep(5000);
    
    /* we must be able to allocate at least two 25MB buffers on top of what you can get from shoot_malloc */
    /* 50D/500D have 27M, 5D3 has 40 */
    for (int i = 0; i < 1000; i++)
    {
        void* buf1 = srm_malloc(25*1024*1024);
        printf("srm_malloc(25M) => %x\n", buf1);
        
        void* buf2 = srm_malloc(25*1024*1024);
        printf("srm_malloc(25M) => %x\n", buf2);

        /* we must be able to free them in any order, even if the backend doesn't allow that */
        if (rand()%2)
        {
            free(buf1);
            free(buf2);
        }
        else
        {
            free(buf2);
            free(buf1);
        }

        if (i == 0)
        {
            /* delay the first iteration, so you can see what's going on */
            /* also save a screenshot */
            msleep(5000);
            take_screenshot(0, SCREENSHOT_BMP);
        }
        
        if (!buf1 || !buf2)
        {
            /* allocation failed? wait before retrying */
            msleep(1000);
        }
    }
    
    printf("SRM memory test completed.\n");
    printf("Are you able to take pictures now? (you should)\n");
    msleep(5000);
    console_hide();
}

static void malloc_test_task()
{
    printf("Small-block malloc test...\n");
    msleep(2000);
    console_show();
    msleep(1000);

    /* allocate up to 50000 small blocks of RAM, 32K each */
    int N = 50000;
    int blocksize = 32*1024;
    void** ptr = malloc(N * sizeof(ptr[0]));
    if (ptr)
    {
        for (int i = 0; i < N; i++)
        {
            ptr[i] = 0;
        }

        for (int i = 0; i < N; i++)
        {
            ptr[i] = malloc(blocksize);
            printf("alloc %d %8x (total %s)\n", i, ptr[i], format_memory_size(i * blocksize));
            if (ptr[i]) memset(ptr[i], rand(), blocksize);
            else break;
        }
        
        msleep(2000);
        
        for (int i = 0; i < N; i++)
        {
            if (ptr[i])
            {
                printf("free %x\n", ptr[i]);
                free(ptr[i]);
                ptr[i] = 0;
            }
        }
    }
    free(ptr);

    printf("Small-block malloc test completed.\n\n");
    printf("You will see an error in the Debug menu,\n");
    printf("on the 'Free Memory' menu item. That's OK.\n");

    msleep(5000);
    console_hide();
}

static void memory_leak_test_task()
{
    printf("Memory leak test...\n");
    msleep(2000);
    console_show();
    msleep(1000);

    /* check for memory leaks */
    for (int i = 0; i < 1000; i++)
    {
        printf("%d/1000\n", i);
        
        /* with this large size, the backend will use fio_malloc, which returns uncacheable pointers */
        void* p = malloc(16*1024*1024 + 64);
        
        if (!p)
        {
            printf("malloc err\n");
            continue;
        }
        
        /* however, user code should not care about this; we have requested a plain old cacheable pointer; did we get one? */
        ASSERT(p == CACHEABLE(p));
        
        /* do something with our memory */
        memset(p, 1234, 1234);
        msleep(20);
        
        /* done, now free it */
        /* the backend should put back the uncacheable flag (if handled incorrectly, there may be memory leaks) */
        free(p);
        msleep(20);
    }
    
    /* if we managed to arrive here, the test ran successfully */
    printf("Memory leak test completed.\n");
    msleep(5000);
    console_hide();
}

static void edmac_test_task()
{
    msleep(2000);
    
    /* this test requires display on */
    
    if (!display_is_on())
    {
        enter_play_mode();
    }
    
    if (!display_is_on())
    {
        beep();
        return;
    }

    uint8_t* real = bmp_vram_real();
    uint8_t* idle = bmp_vram_idle();
    int xPos = 0;
    int xOff = 2;
    int yPos = 0;

    edmac_memcpy_res_lock();
    edmac_copy_rectangle_adv(BMP_VRAM_START(idle), BMP_VRAM_START(real), 960, 120, 50, 960, 120, 50, 720, 440);
    while(true)
    {
        edmac_copy_rectangle_adv(BMP_VRAM_START(real), BMP_VRAM_START(idle), 960, 120, 50, 960, 120+xPos, 50+yPos, 720-xPos, 440-yPos);
        xPos += xOff;

        if(xPos >= 100 || xPos <= -100)
        {
            xOff *= -1;
        }
    }
    edmac_memcpy_res_unlock();
}

static void __attribute__((optimize("-fno-delete-null-pointer-checks")))
null_pointer_task()
{
    msleep(1000);
    console_clear();
    printf("Testing null pointer checker...\n");
    console_show();
    msleep(1000);

    /* find the last crash log number - will trigger a new one */
    char log_filename[100];
    int log_number;
    for (log_number = 99; log_number >= 0; log_number--)
    {
        snprintf(log_filename, sizeof(log_filename), "CRASH%02d.LOG", log_number);
        if (is_file(log_filename))
        {
            ASSERT(log_number < 99);
            break;
        }
    }

    /* this should trigger a crash log */
    /* the error will be noticed when DryOS switches to the next task */
    uint32_t old = cli();
    *(volatile uint32_t *) 0x0 = 0xBAADBAAD;
    printf("MEM(0) = %X %s\n", MEM(0), MEM(0) == 0xBAADBAAD ? "OK" : "ERR");
    sei(old);

    msleep(1000);

    /* this should be restored to the old value */
    printf("MEM(0) = %X %s\n", MEM(0), MEM(0) == 0xBAADBAAD ? "ERR" : "OK");

    /* does the new crash log look sane? */
    snprintf(log_filename, sizeof(log_filename), "CRASH%02d.LOG", log_number + 1);
    if (!is_file(log_filename))
    {
        printf("%s not saved - please report.\n", log_filename);
    }
    else
    {
        /* crash log was saved */
        int size;
        char * log = (char *) read_entire_file(log_filename, &size);
        if (strstr(log, "baadbaad") && strstr(log, "run_test: NULL PTR"))
        {
            printf("%s looks OK.\n", log_filename);
        }
        else
        {
            printf("%s not good - please report.\n", log_filename);
        }
        free(log);
    }

    /* hide the crash log prompt(s) */
    printf("Please wait; ignore any flashing prompts    ");
    for (int i = 0; i <= 100; i++)
    {
        NotifyBoxHide();
        msleep(50);
        printf("\b\b\b(%d)", 5 - i / 20);
    }

    /* finished */
    printf("\nNull pointer test completed.\n");
    msleep(2000);
    console_hide();
}

static void frozen_task()
{
    NotifyBox(2000, "while(1);");
    msleep(3000);
    while(1);
}

static void lockup_task()
{
    NotifyBox(2000, "cli(); while(1);");
    msleep(3000);
    cli();
    while(1);
}

static void freeze_gui_task()
{
    NotifyBox(2000, "GUI task locked up.");
    while(1) msleep(1000);
}

static void divzero_task()
{
    for (int i = -10; i < 10; i++)
    {
        console_show();
        printf("1000/%d = %d = %d\n", i, 1000/i, (int)(1000.0 / (float)i));
        msleep(500);
    }
}

static void alloc_1M_task()
{
    console_show();
    msleep(2000);

    /* after a few calls, this will fail with ERR70 */
    void * ptr = _AllocateMemory(1024 * 1024);

    /* do something with "ptr" to prevent a tail call (to test the stack trace) */
    printf("AllocateMemory 1MB => %x\n", ptr);

    /* do not free it */
}

static void alloc_10M_task()
{
    console_show();
    msleep(2000);

    void * ptr = malloc(10 * 1024 * 1024);
    printf("Alloc 10MB => %x\n", ptr);
    /* do not free it */
}

static struct menu_entry selftest_menu[] =
{
    {
        .name       = "Self tests",
        .select     = menu_open_submenu,
        .help       = "Tests to make sure Magic Lantern is stable and won't crash.",
        .submenu_width = 650,
        .children   = (struct menu_entry[]) {
            {
                .name       = "Stubs API test",
                .select     = run_in_separate_task,
                .priv       = stub_test_task,
                .help       = "Tests Canon functions called by ML. SET=once, PLAY=100x."
            },
            {
                .name       = "RPC reliability test (infinite)",
                .select     = run_in_separate_task,
                .priv       = rpc_test_task,
                .help       = "Flood master with RPC requests and print delay. ",
                .shidden    = 1,    /* 7D only */
            },
            {
                .name       = "Quick test (around 15 min)",
                .select     = run_in_separate_task,
                .priv       = stress_test_task,
                .help       = "A quick test which covers basic functionality. "
            },
            {
                .name       = "Random tests (infinite loop)",
                .select     = run_in_separate_task,
                .priv       = stress_test_random_task,
                .help       = "A thorough test which randomly enables functions from menu. "
            },
            {
                .name       = "Menu backend test (infinite)",
                .select     = run_in_separate_task,
                .priv       = stress_test_menu_dlg_api_task,
                .help       = "Tests proper usage of Canon API calls in ML menu backend."
            },
            {
                .name       = "Redraw test (infinite)",
                .select     = run_in_separate_task,
                .priv       = excessive_redraws_task,
                .help       = "Causes excessive redraws for testing the graphics backend",
            },
            {
                .name       = "Rectangle test (infinite)",
                .select     = run_in_separate_task,
                .priv       = bmp_fill_test_task,
                .help       = "Stresses graphics bandwith. Run this while recording.",
            },
            {
                .name       = "SRM memory test (5 minutes)",
                .select     = run_in_separate_task,
                .priv       = srm_test_task,
                .help       = "Tests SRM memory allocation routines.",
            },
            {
                .name       = "Small-block malloc test (quick)",
                .select     = run_in_separate_task,
                .priv       = malloc_test_task,
                .help       = "Allocate up to 50000 small blocks, 32K each, until memory gets full."
            },
            {
                .name       = "Memory leak test (1 minute)",
                .select     = run_in_separate_task,
                .priv       = memory_leak_test_task,
                .help       = "Allocate and free a large block of RAM (16 MB); repeat 1000 times."
            },
            {
                .name       = "EDMAC screen test (infinite)",
                .select     = run_in_separate_task,
                .priv       = edmac_test_task,
                .help       = "Shift the entire display left and right with EDMAC routines.",
                .help2      = "Fixme: this will lock up if you change the video mode during the test.",
            },
            {
                .name       = "Null pointer test (quick)",
                .select     = run_in_separate_task,
                .priv       = null_pointer_task,
                .help       = "Writes 0xBAADBAAD to address 0 (simulating a null pointer error).",
                .help2      = "ML should save a crash log about 0xBAADBAAD and should not crash.",
            },
            MENU_EOL,
        }
    },
    {
        .name       = "Fault emulation",
        .select     = menu_open_submenu,
        .help       = "Causes intentionally wrong behavior to see DryOS reaction.",
        .help2      = "You'll have to take the battery out after running most of these.",
        .children   = (struct menu_entry[]) {
            {
                .name       = "Create a stuck task",
                .select     = run_in_separate_task,
                .priv       = frozen_task,
                .help       = "Creates a task which will become stuck in an infinite loop.",
                .help2      = "Low priority tasks (prio >= 0x1A) will no longer be able to run.",
            },
            {
                .name       = "Freeze the GUI task",
                .select     = freeze_gui_task,
                .help       = "Freezes main GUI task. Camera will stop reacting to buttons.",
            },
            {
                .name       = "Lock-up the ARM CPU",
                .select     = run_in_separate_task,
                .priv       = lockup_task,
                .help       = "Creates a task which will clear the interrupts and execute while(1).",
                .help2      = "Anything still running after that would be secondary CPUs or hardware.",
            },
            {
                .name       = "Division by zero",
                .select     = run_in_separate_task,
                .priv       = divzero_task,
                .help       = "Performs some math operations which will divide by zero.",
            },
            {
                .name       = "AllocateMemory 1MB",
                .select     = run_in_separate_task,
                .priv       = alloc_1M_task,
                .help       = "Allocates 1MB RAM using AllocateMemory, without freeing it.",
                .help2      = "After running this a few times, you'll get ERR70.",
            },
            {
                .name       = "Allocate 10MB of RAM",
                .select     = run_in_separate_task,
                .priv       = alloc_10M_task,
                .help       = "Allocates 1MB RAM from any source, without freeing it.",
                .help2      = "After running this a few times, you'll run out of memory.",
            },
            MENU_EOL,
        }
    },
};

static struct menu_entry * selftest_menu_entry(const char* entry_name)
{
    /* menu entries are not yet linked, so iterate as in array, not as in linked list */
    for(struct menu_entry * entry = selftest_menu[0].children ; !MENU_IS_EOL(entry) ; entry++ )
    {
        if (streq(entry->name, entry_name))
        {
            return entry;
        }
    }
    return 0;
}

/* fixme: move to core */
static void selftest_menu_show(const char* entry_name)
{
    struct menu_entry * entry = selftest_menu_entry(entry_name);
    if (entry)
    {
        entry->shidden = 0;
    }
    else
    {
        console_show();
        printf("Could not find '%s'\n", entry_name);
    }
}

static unsigned int selftest_init()
{
    BGMT_PLAY        = module_translate_key(MODULE_KEY_PLAY,        MODULE_KEY_CANON);
    BGMT_MENU        = module_translate_key(MODULE_KEY_MENU,        MODULE_KEY_CANON);
    BGMT_INFO        = module_translate_key(MODULE_KEY_INFO,        MODULE_KEY_CANON);
    BGMT_LV          = module_translate_key(MODULE_KEY_LV,          MODULE_KEY_CANON);
    BGMT_PRESS_SET   = module_translate_key(MODULE_KEY_PRESS_SET,   MODULE_KEY_CANON);
    BGMT_WHEEL_LEFT  = module_translate_key(MODULE_KEY_WHEEL_LEFT,  MODULE_KEY_CANON);
    BGMT_WHEEL_RIGHT = module_translate_key(MODULE_KEY_WHEEL_RIGHT, MODULE_KEY_CANON);
    BGMT_WHEEL_UP    = module_translate_key(MODULE_KEY_WHEEL_UP,    MODULE_KEY_CANON);
    BGMT_WHEEL_DOWN  = module_translate_key(MODULE_KEY_WHEEL_DOWN,  MODULE_KEY_CANON);
    BGMT_TRASH       = module_translate_key(MODULE_KEY_TRASH,       MODULE_KEY_CANON);
    
    menu_add("Debug", selftest_menu, COUNT(selftest_menu));
    
    if (is_camera("7D", "*"))
    {
        selftest_menu_show("RPC reliability test (infinite)");
    }
    
    return 0;
}

static unsigned int selftest_deinit()
{
    return 0;
}

MODULE_INFO_START()
    MODULE_INIT(selftest_init)
    MODULE_DEINIT(selftest_deinit)
MODULE_INFO_END()
back to top