/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * Copyright by The HDF Group.                                               *
 * All rights reserved.                                                      *
 *                                                                           *
 * This file is part of HDF5.  The full HDF5 copyright notice, including     *
 * terms governing use, modification, and redistribution, is contained in    *
 * the LICENSE file, which can be found at the root of the source code       *
 * distribution tree, or in https://www.hdfgroup.org/licenses.               *
 * If you do not have access to either file, you may request a copy from     *
 * help@hdfgroup.org.                                                        *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "H5_api_test_util.h"
#include "H5_api_test_parallel.h"

#include "H5_api_attribute_test_parallel.h"
#include "H5_api_dataset_test_parallel.h"
#include "H5_api_datatype_test_parallel.h"
#include "H5_api_file_test_parallel.h"
#include "H5_api_group_test_parallel.h"
#include "H5_api_link_test_parallel.h"
#include "H5_api_misc_test_parallel.h"
#include "H5_api_object_test_parallel.h"
#ifdef H5_API_TEST_HAVE_ASYNC
#include "H5_api_async_test_parallel.h"
#endif

char H5_api_test_parallel_filename[H5_API_TEST_FILENAME_MAX_LENGTH];

const char *test_path_prefix;

int mpi_size;
int mpi_rank;

/* X-macro to define the following for each test:
 * - enum type
 * - name
 * - test function
 * - enabled by default
 */
#ifdef H5_API_TEST_HAVE_ASYNC
#define H5_API_PARALLEL_TESTS                                                                                \
    X(H5_API_TEST_NULL, "", NULL, 0)                                                                         \
    X(H5_API_TEST_FILE, "file", H5_api_file_test_parallel_add, 1)                                            \
    X(H5_API_TEST_GROUP, "group", H5_api_group_test_parallel_add, 1)                                         \
    X(H5_API_TEST_DATASET, "dataset", H5_api_dataset_test_parallel_add, 1)                                   \
    X(H5_API_TEST_DATATYPE, "datatype", H5_api_datatype_test_parallel_add, 1)                                \
    X(H5_API_TEST_ATTRIBUTE, "attribute", H5_api_attribute_test_parallel_add, 1)                             \
    X(H5_API_TEST_LINK, "link", H5_api_link_test_parallel_add, 1)                                            \
    X(H5_API_TEST_OBJECT, "object", H5_api_object_test_parallel_add, 1)                                      \
    X(H5_API_TEST_MISC, "misc", H5_api_misc_test_parallel_add, 1)                                            \
    X(H5_API_TEST_ASYNC, "async", H5_api_async_test_parallel_add, 1)                                         \
    X(H5_API_TEST_MAX, "", NULL, 0)
#else
#define H5_API_PARALLEL_TESTS                                                                                \
    X(H5_API_TEST_NULL, "", NULL, 0)                                                                         \
    X(H5_API_TEST_FILE, "file", H5_api_file_test_parallel_add, 1)                                            \
    X(H5_API_TEST_GROUP, "group", H5_api_group_test_parallel_add, 1)                                         \
    X(H5_API_TEST_DATASET, "dataset", H5_api_dataset_test_parallel_add, 1)                                   \
    X(H5_API_TEST_DATATYPE, "datatype", H5_api_datatype_test_parallel_add, 1)                                \
    X(H5_API_TEST_ATTRIBUTE, "attribute", H5_api_attribute_test_parallel_add, 1)                             \
    X(H5_API_TEST_LINK, "link", H5_api_link_test_parallel_add, 1)                                            \
    X(H5_API_TEST_OBJECT, "object", H5_api_object_test_parallel_add, 1)                                      \
    X(H5_API_TEST_MISC, "misc", H5_api_misc_test_parallel_add, 1)                                            \
    X(H5_API_TEST_MAX, "", NULL, 0)
#endif

#define X(a, b, c, d) a,
enum H5_api_test_type { H5_API_PARALLEL_TESTS };
#undef X
#define X(a, b, c, d) b,
static const char *const H5_api_test_name[] = {H5_API_PARALLEL_TESTS};
#undef X
#define X(a, b, c, d) c,
static void (*H5_api_test_add_func[])(void) = {H5_API_PARALLEL_TESTS};
#undef X
#define X(a, b, c, d) d,
static int H5_api_test_enabled[] = {H5_API_PARALLEL_TESTS};
#undef X

static enum H5_api_test_type
H5_api_test_name_to_type(const char *test_name)
{
    enum H5_api_test_type i = 0;

    while (strcmp(H5_api_test_name[i], test_name) && i != H5_API_TEST_MAX)
        i++;

    return ((i == H5_API_TEST_MAX) ? H5_API_TEST_NULL : i);
}

static void
H5_api_test_add(void)
{
    enum H5_api_test_type i;

    for (i = H5_API_TEST_FILE; i < H5_API_TEST_MAX; i++)
        if (H5_api_test_enabled[i])
            H5_api_test_add_func[i]();
}

hid_t
create_mpi_fapl(MPI_Comm comm, MPI_Info info, bool coll_md_read)
{
    hid_t ret_pl = H5I_INVALID_HID;

    MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);

    if ((ret_pl = H5Pcreate(H5P_FILE_ACCESS)) < 0)
        goto error;

    if (H5Pset_fapl_mpio(ret_pl, comm, info) < 0)
        goto error;
    if (H5Pset_all_coll_metadata_ops(ret_pl, coll_md_read) < 0)
        goto error;
    if (H5Pset_coll_metadata_write(ret_pl, true) < 0)
        goto error;

    return ret_pl;

error:
    return H5I_INVALID_HID;
} /* end create_mpi_fapl() */

/*
 * Generates random dimensions for a dataspace. The first dimension
 * is always `mpi_size` to allow for convenient subsetting; the rest
 * of the dimensions are randomized.
 */
int
generate_random_parallel_dimensions(int space_rank, hsize_t **dims_out)
{
    hsize_t *dims = NULL;
    size_t   i;

    if (space_rank <= 0)
        goto error;

    if (NULL == (dims = malloc((size_t)space_rank * sizeof(hsize_t))))
        goto error;
    if (MAINPROCESS) {
        for (i = 0; i < (size_t)space_rank; i++) {
            if (i == 0)
                dims[i] = (hsize_t)mpi_size;
            else
                dims[i] = (hsize_t)((rand() % MAX_DIM_SIZE) + 1);
        }
    }

    /*
     * Ensure that the dataset dimensions are uniform across ranks.
     */
    if (MPI_SUCCESS != MPI_Bcast(dims, space_rank, MPI_UNSIGNED_LONG_LONG, 0, MPI_COMM_WORLD))
        goto error;

    *dims_out = dims;

    return 0;

error:
    if (dims)
        free(dims);

    return -1;
}

static int
parse_command_line(int argc, char **argv)
{
    /* Simple argument checking, TODO can improve that later */
    if (argc > 1) {
        enum H5_api_test_type i = H5_api_test_name_to_type(argv[argc - 1]);
        if (i != H5_API_TEST_NULL) {
            /* Run only specific API test */
            memset(H5_api_test_enabled, 0, sizeof(H5_api_test_enabled));
            H5_api_test_enabled[i] = 1;
        }
    }

    return 0;
}

static void
usage(FILE *stream)
{
    fprintf(stream, "file        run only the file interface tests\n");
    fprintf(stream, "group       run only the group interface tests\n");
    fprintf(stream, "dataset     run only the dataset interface tests\n");
    fprintf(stream, "attribute   run only the attribute interface tests\n");
    fprintf(stream, "datatype    run only the datatype interface tests\n");
    fprintf(stream, "link        run only the link interface tests\n");
    fprintf(stream, "object      run only the object interface tests\n");
    fprintf(stream, "misc        run only the miscellaneous tests\n");
    fprintf(stream, "async       run only the async interface tests\n");
}

int
main(int argc, char **argv)
{
    H5E_auto2_t default_err_func;
    const char *vol_connector_string;
    const char *vol_connector_name;
    unsigned    seed;
    hid_t       fapl_id                   = H5I_INVALID_HID;
    hid_t       default_con_id            = H5I_INVALID_HID;
    hid_t       registered_con_id         = H5I_INVALID_HID;
    char       *vol_connector_string_copy = NULL;
    char       *vol_connector_info        = NULL;
    void       *default_err_data          = NULL;
    int         required                  = MPI_THREAD_MULTIPLE;
    int         provided;

    /*
     * Attempt to initialize with MPI_THREAD_MULTIPLE for VOL connectors
     * that require that level of threading support in MPI
     */
    if (MPI_SUCCESS != MPI_Init_thread(&argc, &argv, required, &provided)) {
        fprintf(stderr, "MPI_Init_thread failed\n");
        exit(EXIT_FAILURE);
    }

    MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
    MPI_Comm_size(MPI_COMM_WORLD, &mpi_size);

    if (provided < required) {
        if (MAINPROCESS)
            printf("** INFO: couldn't initialize with MPI_THREAD_MULTIPLE threading support **\n");
    }

    /*
     * Make sure that HDF5 is initialized on all MPI ranks before proceeding.
     * This is important for certain VOL connectors which may require a
     * collective initialization.
     */
    H5open();

    /* Store current error stack printing function since TestInit unsets it */
    H5Eget_auto2(H5E_DEFAULT, &default_err_func, &default_err_data);

    /* Initialize testing framework */
    if (TestInit(argv[0], usage, NULL, NULL, NULL, mpi_rank) < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "Couldn't initialize testing framework\n");
        goto error;
    }

    /* Reset error stack printing function */
    H5Eset_auto2(H5E_DEFAULT, default_err_func, default_err_data);

    /* Hide all output from testing framework and replace with our own */
    SetTestVerbosity(VERBO_NONE);

    /* Parse command line separately from the test framework since
     * tests need to be added before TestParseCmdLine in order for
     * the -help option to show them, but we need to know ahead of
     * time which tests to add if only a specific interface's tests
     * are going to be run.
     */
    parse_command_line(argc, argv);

    /* Add tests */
    H5_api_test_add();

    /* Display testing information */
    TestInfo(stdout);

    /* Parse command line arguments */
    if (TestParseCmdLine(argc, argv) < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "Couldn't parse command-line arguments\n");
        goto error;
    }

    n_tests_run_g     = 0;
    n_tests_passed_g  = 0;
    n_tests_failed_g  = 0;
    n_tests_skipped_g = 0;

    if (MAINPROCESS) {
        seed = (unsigned)time(NULL);
    }

    if (mpi_size > 1) {
        if (MPI_SUCCESS != MPI_Bcast(&seed, 1, MPI_UNSIGNED, 0, MPI_COMM_WORLD)) {
            if (MAINPROCESS)
                fprintf(stderr, "Couldn't broadcast test seed\n");
            goto error;
        }
    }

    srand(seed);

    if (NULL == (test_path_prefix = getenv(HDF5_API_TEST_PATH_PREFIX)))
        test_path_prefix = "";

    snprintf(H5_api_test_parallel_filename, H5_API_TEST_FILENAME_MAX_LENGTH, "%s%s", test_path_prefix,
             PARALLEL_TEST_FILE_NAME);

    if (NULL == (vol_connector_string = getenv(HDF5_VOL_CONNECTOR))) {
        if (MAINPROCESS)
            printf("No VOL connector selected; using native VOL connector\n");
        vol_connector_name = "native";
        vol_connector_info = NULL;
    }
    else {
        char *token = NULL;

        BEGIN_INDEPENDENT_OP(copy_connector_string)
        {
            if (NULL == (vol_connector_string_copy = strdup(vol_connector_string))) {
                if (MAINPROCESS)
                    fprintf(stderr, "Unable to copy VOL connector string\n");
                INDEPENDENT_OP_ERROR(copy_connector_string);
            }
        }
        END_INDEPENDENT_OP(copy_connector_string);

        BEGIN_INDEPENDENT_OP(get_connector_name)
        {
            if (NULL == (token = strtok(vol_connector_string_copy, " "))) {
                if (MAINPROCESS)
                    fprintf(stderr, "Error while parsing VOL connector string\n");
                INDEPENDENT_OP_ERROR(get_connector_name);
            }
        }
        END_INDEPENDENT_OP(get_connector_name);

        vol_connector_name = token;

        if (NULL != (token = strtok(NULL, " "))) {
            vol_connector_info = token;
        }
    }

    if (MAINPROCESS) {
        printf("Running parallel API tests with VOL connector '%s' and info string '%s'\n\n",
               vol_connector_name, vol_connector_info ? vol_connector_info : "");
        printf("Test parameters:\n");
        printf("  - Test file name: '%s'\n", H5_api_test_parallel_filename);
        printf("  - Number of MPI ranks: %d\n", mpi_size);
        printf("  - Test seed: %u\n", seed);
        printf("\n");
    }

    BEGIN_INDEPENDENT_OP(create_fapl)
    {
        if ((fapl_id = create_mpi_fapl(MPI_COMM_WORLD, MPI_INFO_NULL, false)) < 0) {
            if (MAINPROCESS)
                fprintf(stderr, "Unable to create FAPL\n");
            INDEPENDENT_OP_ERROR(create_fapl);
        }
    }
    END_INDEPENDENT_OP(create_fapl);

    BEGIN_INDEPENDENT_OP(check_vol_register)
    {
        /*
         * If using a VOL connector other than the native
         * connector, check whether the VOL connector was
         * successfully registered before running the tests.
         * Otherwise, HDF5 will default to running the tests
         * with the native connector, which could be misleading.
         */
        if (0 != strcmp(vol_connector_name, "native")) {
            htri_t is_registered;

            if ((is_registered = H5VLis_connector_registered_by_name(vol_connector_name)) < 0) {
                if (MAINPROCESS)
                    fprintf(stderr, "Unable to determine if VOL connector is registered\n");
                INDEPENDENT_OP_ERROR(check_vol_register);
            }

            if (!is_registered) {
                if (MAINPROCESS)
                    fprintf(stderr, "Specified VOL connector '%s' wasn't correctly registered!\n",
                            vol_connector_name);
                INDEPENDENT_OP_ERROR(check_vol_register);
            }
            else {
                int cmp = 0;

                /*
                 * If the connector was successfully registered, check that
                 * the connector ID set on the default FAPL matches the ID
                 * for the registered connector before running the tests.
                 */
                if (H5Pget_vol_id(fapl_id, &default_con_id) < 0) {
                    if (MAINPROCESS)
                        fprintf(stderr, "Couldn't retrieve ID of VOL connector set on default FAPL\n");
                    INDEPENDENT_OP_ERROR(check_vol_register);
                }

                if ((registered_con_id = H5VLget_connector_id_by_name(vol_connector_name)) < 0) {
                    if (MAINPROCESS)
                        fprintf(stderr, "Couldn't retrieve ID of registered VOL connector\n");
                    INDEPENDENT_OP_ERROR(check_vol_register);
                }

                if (H5VLcmp_connector_cls(&cmp, default_con_id, registered_con_id) < 0) {
                    if (MAINPROCESS)
                        fprintf(stderr, "Couldn't compare VOL connector classes\n");
                    INDEPENDENT_OP_ERROR(check_vol_register);
                }

                if (0 != cmp) {
                    if (MAINPROCESS)
                        fprintf(stderr,
                                "VOL connector set on default FAPL didn't match specified VOL connector\n");
                    INDEPENDENT_OP_ERROR(check_vol_register);
                }
            }
        }
    }
    END_INDEPENDENT_OP(check_vol_register);

    /* Retrieve the VOL cap flags - work around an HDF5
     * library issue by creating a FAPL
     */
    BEGIN_INDEPENDENT_OP(get_capability_flags)
    {
        vol_cap_flags_g = H5VL_CAP_FLAG_NONE;
        if (H5Pget_vol_cap_flags(fapl_id, &vol_cap_flags_g) < 0) {
            if (MAINPROCESS)
                fprintf(stderr, "Unable to retrieve VOL connector capability flags\n");
            INDEPENDENT_OP_ERROR(get_capability_flags);
        }
    }
    END_INDEPENDENT_OP(get_capability_flags);

    /*
     * Create the file that will be used for all of the tests,
     * except for those which test file creation.
     */
    BEGIN_INDEPENDENT_OP(create_test_container)
    {
        if (MAINPROCESS) {
            if (create_test_container(H5_api_test_parallel_filename, vol_cap_flags_g) < 0) {
                fprintf(stderr, "    failed to create testing container file '%s'\n",
                        H5_api_test_parallel_filename);
                INDEPENDENT_OP_ERROR(create_test_container);
            }
        }
    }
    END_INDEPENDENT_OP(create_test_container);

    MPI_Barrier(MPI_COMM_WORLD);

    /* Perform tests */
    if (PerformTests() < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "Couldn't run tests\n");
        goto error;
    }

    if (MAINPROCESS)
        printf("\n");

    if (MAINPROCESS) {
        /* Display test summary, if requested */
        if (GetTestSummary())
            TestSummary(stdout);

        printf("Deleting container file for tests\n\n");
    }

    if (GetTestCleanup())
        H5Fdelete(H5_api_test_parallel_filename, fapl_id);

    if (n_tests_run_g > 0) {
        if (MAINPROCESS)
            printf("The below statistics are minimum values due to the possibility of some ranks failing a "
                   "test while others pass:\n");

        if (MPI_SUCCESS != MPI_Allreduce(MPI_IN_PLACE, &n_tests_passed_g, 1, H5_SIZE_T_AS_MPI_TYPE, MPI_MIN,
                                         MPI_COMM_WORLD)) {
            if (MAINPROCESS)
                printf("    failed to collect consensus about the minimum number of tests that passed -- "
                       "reporting rank 0's (possibly inaccurate) value\n");
        }

        if (MAINPROCESS)
            printf("%s%zu/%zu (%.2f%%) API tests passed across all ranks with VOL connector '%s'\n",
                   n_tests_passed_g > 0 ? "At least " : "", n_tests_passed_g, n_tests_run_g,
                   ((double)n_tests_passed_g / (double)n_tests_run_g * 100.0), vol_connector_name);

        if (MPI_SUCCESS != MPI_Allreduce(MPI_IN_PLACE, &n_tests_failed_g, 1, H5_SIZE_T_AS_MPI_TYPE, MPI_MIN,
                                         MPI_COMM_WORLD)) {
            if (MAINPROCESS)
                printf("    failed to collect consensus about the minimum number of tests that failed -- "
                       "reporting rank 0's (possibly inaccurate) value\n");
        }

        if (MAINPROCESS) {
            printf("%s%zu/%zu (%.2f%%) API tests did not pass across all ranks with VOL connector '%s'\n",
                   n_tests_failed_g > 0 ? "At least " : "", n_tests_failed_g, n_tests_run_g,
                   ((double)n_tests_failed_g / (double)n_tests_run_g * 100.0), vol_connector_name);

            printf("%zu/%zu (%.2f%%) API tests were skipped with VOL connector '%s'\n", n_tests_skipped_g,
                   n_tests_run_g, ((double)n_tests_skipped_g / (double)n_tests_run_g * 100.0),
                   vol_connector_name);
        }
    }

    if (default_con_id >= 0 && H5VLclose(default_con_id) < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "    failed to close VOL connector ID\n");
    }

    if (registered_con_id >= 0 && H5VLclose(registered_con_id) < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "    failed to close VOL connector ID\n");
    }

    if (fapl_id >= 0 && H5Pclose(fapl_id) < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "    failed to close MPI FAPL\n");
    }

    free(vol_connector_string_copy);
    vol_connector_string_copy = NULL;

    if (GetTestNumErrs() > 0)
        n_tests_failed_g += (size_t)GetTestNumErrs();

    /* Release test infrastructure */
    if (TestShutdown() < 0) {
        if (MAINPROCESS)
            fprintf(stderr, "Couldn't shut down testing framework\n");
        goto error;
    }

    H5close();

    MPI_Finalize();

    /* Exit failure if errors encountered; else exit success. */
    if (n_tests_failed_g > 0)
        exit(EXIT_FAILURE);
    else
        exit(EXIT_SUCCESS);

error:
    free(vol_connector_string_copy);

    H5E_BEGIN_TRY
    {
        H5VLclose(default_con_id);
        H5VLclose(registered_con_id);
        H5Pclose(fapl_id);
    }
    H5E_END_TRY

    MPI_Finalize();

    exit(EXIT_FAILURE);
}
