Newer
Older
mbed-os / drivers / usb / tests / TESTS / usb_device / msd / main.cpp
/*
 * Copyright (c) 2019-2020, Arm Limited and affiliates.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#if !defined(MBED_CONF_RTOS_PRESENT)
#error [NOT_SUPPORTED] USB stack and test cases require RTOS to run.
#else

#include <stdio.h>
#include <string.h>
#include <stdlib.h>     /* srand, rand */
#include "greentea-client/test_env.h"
#include "unity/unity.h"
#include "utest/utest.h"

#include "mbed.h"
#include "USBMSD.h"
#include "TestUSBMSD.h"

#include "HeapBlockDevice.h"
#include "FATFileSystem.h"

// TARGET_NANO100 SRAM 16KB can't afford mass-storage-disk test, so skip usb_msd_test.
#if !defined(DEVICE_USBDEVICE) || !DEVICE_USBDEVICE || TARGET_NANO100
#error [NOT_SUPPORTED] USB Device not supported for this target
#else


#define OS_WINDOWS  1
#define OS_LINUX    2
#define OS_MAC      3

// Host side unmount was disabled for windows machines.
// PowerShell execution policies/restrictions cause that
// on some windows machines unmount is failing
// To re-enable it comment out below line.
#define DISABLE_HOST_SIDE_UMOUNT

#ifdef MIN
#undef MIN
#endif
#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))

#define DEFAULT_BLOCK_SIZE  512
#define HEAP_BLOCK_DEVICE_SIZE (128 * DEFAULT_BLOCK_SIZE)
#define MIN_HEAP_SIZE (HEAP_BLOCK_DEVICE_SIZE + 6144)


/* TODO:
 *
 * Test if slave(DUT) can force host to refresh mounted fs. (not supported in USBMSD yet)
 *
 */

#define TEST_DIR    "usb_msd_test_data"
#define TEST_FILE   "usb_msd_test_file"
#define TEST_STRING "usb_msd_test_string"

using namespace utest::v1;

uint32_t prev_read_counter = 0;
uint32_t prev_program_counter = 0;
extern uint32_t mbed_heap_size;

static char _key[256] = { 0 };
static char _value[128] = { 0 };

static volatile bool msd_process_done = false;


FATFileSystem heap_fs("heap_fs");

Semaphore media_remove_event(0, 1);


/** Creates heap block device
 *
 */
BlockDevice *get_heap_block_device()
{
    // create 64kB heap block device
    if (mbed_heap_size >= MIN_HEAP_SIZE) {
        static HeapBlockDevice bd(128 * DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE);
        bd.init();
        return &bd;
    } else {
        return NULL;
    }
}

uint64_t get_fs_mount_size(FileSystem *fs)
{
    struct statvfs stat;
    fs->statvfs(fs->getName(), &stat);
    uint64_t size = stat.f_bsize * stat.f_blocks;
    return size;
}

/**
 * Create test data
 *
 * @param fs_root filesystem path
 */
static bool test_files_create(const char *fs_root, const char *test_file = TEST_FILE, const char *test_string = TEST_STRING)
{
    char path[128];
    sprintf(path, "/%s/%s", fs_root, TEST_DIR);
    int ret = mkdir(path, 0777);
    if (ret != 0 && errno != EEXIST) {
        utest_printf("mkdir failed!!! errno: %d\n", errno);
        return false;
    }
    sprintf(path, "/%s/%s/%s", fs_root, TEST_DIR, test_file);
    FILE *f = fopen(path, "w");
    if (f == NULL) {
        utest_printf("fopen failed!!! errno: %d\n", errno);
        return false;
    }
    fprintf(f, test_string);
    fflush(f);
    fclose(f);
    return true;
}

/**
 * Remove test data
 *
 * @param fs_root filesystem path
 */
static void test_files_remove(const char *fs_root)
{
    DIR *dir;
    struct dirent *dp;
    char path[512];

    sprintf(path, "/%s/%s", fs_root, TEST_DIR);
    dir = opendir(path);
    if (dir == NULL) {
        return;
    }
    while ((dp = readdir(dir)) != NULL) {
        sprintf(path, "/%s/%s/%s", fs_root, TEST_DIR, dp->d_name);
        remove(path);
    }
    sprintf(path, "/%s/%s", fs_root, TEST_DIR);
    remove(path);
}

/**
 * Check if test data exist
 *
 * @param fs_root filesystem path
 * @return true if data exist
 */
static bool test_files_exist(const char *fs_root, const char *test_file = TEST_FILE, const char *test_string = TEST_STRING)
{
    char path[128];
    char str[512] = { 0 };
    sprintf(path, "/%s/%s/%s", fs_root, TEST_DIR, test_file);
    FILE *f = fopen(path, "r");
    if (f != NULL) {
        fscanf(f, "%s", str);
        if (strcmp(test_string, str) == 0) {
            return true;
        }
    }
    return false;
}

/**
 * Mounts a filesystem to a block device
 *
 * @param bd block device
 * @param fs filesystem
 * @return true if success, false otherwise
 */
static bool prepare_storage(BlockDevice *bd, FileSystem *fs)
{
    const char *fs_root = fs->getName();
    int err = fs->mount(bd);
    if (err) {
        utest_printf("%s filesystem mount failed\ntry to reformat device... ", fs->getName());
        err = fs->reformat(bd);
        if (err) {
            utest_printf("failed !!!\n");
            return false;
        } else {
            utest_printf("succeed\n");
        }
    }
    // remove old test data
    test_files_remove(fs_root);

    return true;
}

void run_processing(Semaphore *sem)
{
    sem->release();
}

void msd_process(USBMSD *msd)
{
    Semaphore proc;
    msd->attach(callback(run_processing, &proc));
    while (!msd_process_done) {
        proc.try_acquire_for(100);
        msd->process();
        if (msd->media_removed()) {
            media_remove_event.release();
        }
    }
    msd->attach(NULL);
}


// wait until msd negotiation is done (no r/w disk operation for at least 1s)
// max wait time is 15s
#define WAIT_MSD_COMMUNICATION_DONE() \
    for (int x = 0; x < 15; x++) { \
        prev_read_counter = usb.get_read_counter();\
        prev_program_counter = usb.get_program_counter();\
        ThisThread::sleep_for(1000);\
        if ((usb.get_read_counter() == prev_read_counter) && \
            (usb.get_program_counter() == prev_program_counter)) {\
            break;\
        }\
    }


#define TEST_ASSERT_EQUAL_STRING_LOOP(expected, actual, loop_index)                             \
        if (strcmp(expected, actual) != 0) {                                                    \
            char str[128];                                                                      \
            sprintf(str, "expected %s was %s (loop index: %lu)", expected, actual, loop_index); \
            TEST_ASSERT_MESSAGE(false, str);                                                    \
        }

#define TEST_ASSERT_EQUAL_LOOP(expected, actual, loop_index)                                    \
        if (expected != actual) {                                                               \
            char str[128];                                                                      \
            sprintf(str, "expected %d was %d (loop index: %lu)", expected, actual, loop_index); \
            TEST_ASSERT_MESSAGE(false, str);                                                    \
        }


/** Initialize storages
 *
 * Given the DUT USB mass storage device
 * When DUT has enought heap memory
 * Then initialize heap block device for tests
 * When DUT has any falsh block device
 * Then initialize it for tests
 */
void storage_init()
{
    TEST_SKIP_UNLESS_MESSAGE(mbed_heap_size >= MIN_HEAP_SIZE, "Not enough heap memory for HeapBlockDevice creation");
    FATFileSystem::format(get_heap_block_device());
    bool result = prepare_storage(get_heap_block_device(), &heap_fs);
    TEST_ASSERT_MESSAGE(result, "heap storage initialisation failed");
}

/** Test mass storage device mount and unmount
 *
 * Given the DUT USB mass storage device connected to the host
 * When DUT call @USBMSD::connect
 * Then host detects mass storage device is mounted as removable disk drive and it reports valid filesystem size
 * When DUT call @USBMSD::disconnect
 * Then host detects mass storage device is unmounted (ejected)
 *
 * Given the DUT USB mass storage device connected to the host and mounted
 * When host unmounts (ejects) mass storage device
 * Then DUT detects media remove event
 */
template <uint32_t N>
void mount_unmount_test(BlockDevice *bd, FileSystem *fs)
{
    Thread msd_thread(osPriorityHigh);
    TestUSBMSD usb(bd, false);
    msd_process_done = false;
    msd_thread.start(callback(msd_process, &usb));

    for (uint32_t i = 1; i <= N; i++) {
        // mount
        usb.connect();
        WAIT_MSD_COMMUNICATION_DONE();
        // check if device is mounted on host side
        greentea_send_kv("check_if_mounted", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));

        TEST_ASSERT_EQUAL_STRING_LOOP("passed", _key, i);

        greentea_send_kv("get_mounted_fs_size", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
        uint64_t ret_size = atoll(_key);
        TEST_ASSERT_EQUAL_UINT64(get_fs_mount_size(fs), ret_size);

        // unmount
        usb.disconnect();
        // check if device is detached on host side
        greentea_send_kv("check_if_not_mounted", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
        TEST_ASSERT_EQUAL_STRING_LOOP("passed", _key, i);
    }

    for (uint32_t i = 1; i <= N; i++) {
        // mount
        usb.connect();
        WAIT_MSD_COMMUNICATION_DONE();
        // check if device is mounted on host side
        greentea_send_kv("check_if_mounted", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
        TEST_ASSERT_EQUAL_STRING_LOOP("passed", _key, i);

        greentea_send_kv("get_mounted_fs_size", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
        uint64_t ret_size = atoll(_key);
        TEST_ASSERT_EQUAL_UINT64(get_fs_mount_size(fs), ret_size);

#ifdef DISABLE_HOST_SIDE_UMOUNT
        greentea_send_kv("get_os_type", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
        int32_t os_type = atoi(_value);
        if (os_type != OS_WINDOWS) {
#endif
            // unmount msd device on host side
            greentea_send_kv("unmount", 0);
            greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
            TEST_ASSERT_EQUAL_STRING_LOOP("passed", _key, i);

            // wait for unmount event (set 10s timeout)
            media_remove_event.try_acquire_for(10000);
            if (!usb.media_removed()) {
                TEST_ASSERT_EQUAL_LOOP(true, usb.media_removed(), i);
            }

            // unmount since media_removed doesn't disconnects device side
            usb.disconnect();
#ifdef DISABLE_HOST_SIDE_UMOUNT
        } else {
            // unmount
            usb.disconnect();
        }
#endif
        // check if device is detached on host side
        greentea_send_kv("check_if_not_mounted", 0);
        greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
        TEST_ASSERT_EQUAL_STRING_LOOP("passed", _key, i);
    }

    // mount
    usb.connect();
    WAIT_MSD_COMMUNICATION_DONE();
    // check if device is mounted on host side
    greentea_send_kv("check_if_mounted", 0);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    // unmount
    usb.disconnect();
    // check if device is detached on host side
    greentea_send_kv("check_if_not_mounted", 0);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    msd_process_done = true; // terminate msd_thread
    msd_thread.join();
}

/** Test mass storage device mount and unmount together with underlying file system operations
 *
 * Given the DUT USB mass storage device connected to the host
 * When DUT call @USBMSD::connect
 * Then host detects that mass storage device is mounted as removable disk drive and test files are present
 * When DUT call @USBMSD::disconnect
 * Then host detects mass storage device is unmounted (ejected)
 *
 * Given the DUT USB mass storage device connected to the host and already mounted
 * When host unmounts (ejects) mass storage device
 * Then DUT detects media remove event
 */
void mount_unmount_and_data_test(BlockDevice *bd, FileSystem *fs)
{
    const char *fs_root = fs->getName();
    Thread msd_thread(osPriorityHigh);
    TestUSBMSD usb(bd, false);
    msd_process_done = false;
    msd_thread.start(callback(msd_process, &usb));

    // mount
    usb.connect();
    WAIT_MSD_COMMUNICATION_DONE();
    // check if device is mounted on host side
    greentea_send_kv("check_if_mounted", 0);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    greentea_send_kv("check_file_exist", TEST_DIR " " TEST_FILE " " TEST_STRING);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("non-exist", _key);

    usb.disconnect();
    // check if device is detached on host side
    greentea_send_kv("check_if_not_mounted", 0);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    test_files_create(fs_root);
    TEST_ASSERT(test_files_exist(fs_root));

    usb.connect();
    WAIT_MSD_COMMUNICATION_DONE();
    // check if device is mounted on host side
    greentea_send_kv("check_if_mounted", 0);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    greentea_send_kv("check_file_exist", TEST_DIR " " TEST_FILE " " TEST_STRING);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("exist", _key);

    greentea_send_kv("delete_files", TEST_DIR " " TEST_FILE);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    do {
        ThisThread::sleep_for(1);
    } while (test_files_exist(fs_root));
    TEST_ASSERT_EQUAL(false, test_files_exist(fs_root));

    usb.disconnect();
    // check if device is detached on host side
    greentea_send_kv("check_if_not_mounted", 0);
    greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value));
    TEST_ASSERT_EQUAL_STRING("passed", _key);

    msd_process_done = true;    // terminate msd_thread
    msd_thread.join();
    test_files_remove(fs_root);
}

void heap_block_device_mount_unmount_test()
{
    TEST_SKIP_UNLESS_MESSAGE(mbed_heap_size >= MIN_HEAP_SIZE, "Not enough heap memory for HeapBlockDevice creation");
    mount_unmount_test<3>(get_heap_block_device(), &heap_fs);
}

void heap_block_device_mount_unmount_and_data_test()
{
    TEST_SKIP_UNLESS_MESSAGE(mbed_heap_size >= MIN_HEAP_SIZE, "Not enough heap memory for HeapBlockDevice creation");
    mount_unmount_and_data_test(get_heap_block_device(), &heap_fs);
}


Case cases[] = {
    Case("storage initialization", storage_init),

    Case("mount/unmount test - Heap block device", heap_block_device_mount_unmount_test),
    Case("mount/unmount and data test - Heap block device", heap_block_device_mount_unmount_and_data_test),
};

utest::v1::status_t greentea_test_setup(const size_t number_of_cases)
{
    GREENTEA_SETUP(300, "pyusb_msd");
    utest_printf("mbed_heap_size %u MIN_HEAP_SIZE %u\n", mbed_heap_size, MIN_HEAP_SIZE);
    utest::v1::status_t status = greentea_test_setup_handler(number_of_cases);
    TestUSBMSD::setup_serial_number();
    return status;
}

Specification specification(greentea_test_setup, cases, greentea_test_teardown_handler);

int main()
{
    Harness::run(specification);
}

#endif // !defined(DEVICE_USBDEVICE) || !DEVICE_USBDEVICE
#endif // !defined(MBED_CONF_RTOS_PRESENT)