diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c960d1..5a02aea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,12 @@ elseif(MSVC) $<$:/W4> # Warning level 4 $<$:/std:c11> # MSVC uses C11 mode for C99 features ) + # Silence MSVC deprecation warnings for the standard C functions this project uses + # (C4996 on strncpy/strcpy/sscanf/fopen/gmtime, and on the POSIX name strdup). + add_compile_definitions( + _CRT_SECURE_NO_WARNINGS + _CRT_NONSTDC_NO_WARNINGS + ) endif() # Find HDF5 diff --git a/parcel.h b/parcel.h index b4140bf..2f37f57 100644 --- a/parcel.h +++ b/parcel.h @@ -187,6 +187,10 @@ typedef struct { /* Iteration encoding type */ pmd_iteration_encoding iteration_encoding; + /* Zero padding width applied to %T when it is expanded into a path or filename + * 0 means no padding, N means expand to at least N digits (e.g. 5 gives "/data/00001/") */ + unsigned int iteration_padding; + /* Optional paths (private - use accessor functions) */ char *_particles_path; /* e.g., "particles/" */ char *_meshes_path; /* e.g., "meshes/" */ @@ -741,13 +745,14 @@ typedef struct { char *scan_parent; char *first_segment; const char *full_pattern; + const char *rest; /* Part of full_pattern after first_segment, e.g. "/step_%T/" */ } iteration_pattern; static pmd_status parse_iteration_pattern(const char *pattern, iteration_pattern *info); static void free_iteration_pattern(iteration_pattern *info); static pmd_status extract_iteration_from_name(const char *name, const char *pattern, - int64_t *iteration_out); -static char* replace_iteration(const char *pattern, int64_t iteration); + int64_t *iteration_out, unsigned int *padding_out); +static char* replace_iteration(const char *pattern, int64_t iteration, unsigned int padding); /* ========================================================================= * C99-Compliant String Utilities @@ -1520,6 +1525,286 @@ static pmd_status ensure_parent_groups(hid_t file_id, const char *path) { return PMD_SUCCESS; } +/** + * Create the group structure above the iteration groups of a GROUP_BASED series + * Lets pmd_list_iterations enumerate groups before any iteration exists, e.g. creates "/data" + * for a base path of "/data/%T/" + */ +static pmd_status ensure_base_path_groups(hid_t file_id, const char *base_path) { + iteration_pattern pattern_info; + + pmd_status status = parse_iteration_pattern(base_path, &pattern_info); + if (status != PMD_SUCCESS) { + return status; + } + + /* Base path has no groups above the iteration groups */ + if (strcmp(pattern_info.scan_parent, ".") == 0) { + free_iteration_pattern(&pattern_info); + return PMD_SUCCESS; + } + + status = ensure_parent_groups(file_id, pattern_info.scan_parent); + if (status == PMD_SUCCESS) { + /* Create the scan_parent group itself */ + htri_t exists = H5Lexists(file_id, pattern_info.scan_parent, H5P_DEFAULT); + if (exists == 0) { + hid_t group_id = H5Gcreate(file_id, pattern_info.scan_parent, + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + if (group_id >= 0) { + H5Gclose(group_id); + } + } + } + + free_iteration_pattern(&pattern_info); + return status; +} + +typedef struct { + int found; /* Whether a name matching the pattern was found */ + int64_t iteration; /* Iteration index of the first matching name */ + unsigned int padding; /* Zero padding width of the first matching name */ +} iteration_group_match; + +typedef struct { + const char *segment; /* Pattern for the group names at this level, e.g. "step_%T" */ + int64_t iteration; /* Iteration the enclosing groups were matched with */ + int mismatch; /* Set when a name holds that iteration under a different padding */ +} nested_padding_probe; + +/** + * Callback for H5Literate that looks for the iteration under a different zero padding + */ +static herr_t nested_iteration_padding_callback(hid_t loc_id, const char *name, + const H5L_info_t *info, void *op_data) { + nested_padding_probe *probe = (nested_padding_probe *)op_data; + int64_t iteration; + unsigned int padding; + + (void)loc_id; + (void)info; + + pmd_status status = extract_iteration_from_name(name, probe->segment, &iteration, &padding); + if (status == PMD_ERROR_FILE_FORMAT || (status == PMD_SUCCESS && iteration == probe->iteration)) { + /* The name expected for this padding is missing, so a match here is padded differently */ + probe->mismatch = 1; + return 1; + } + + return 0; /* Name doesn't match the iteration, keep looking */ +} + +/** + * Look for a zero padding mismatch in the groups below an iteration group + * + * Walks the pattern segments after the first %T segment (e.g. "/step_%T/" of "/data/%T/step_%T/") + * below the group name, expanding %T with padding. Reports PMD_ERROR_FILE_FORMAT when a group holds + * the same iteration under a different padding. Reports PMD_SUCCESS otherwise, whether or not the + * expected path resolves, since a missing path is not this function's concern. + * + * @param loc_id Group holding the iteration groups, e.g. "/data" for a base path of "/data/%T/" + * @param name Name of the matched iteration group, e.g. "0001" + * @param rest Part of the pattern after the first %T segment + * @param iteration Iteration index the group name matched + * @param padding Zero padding width the group name matched with + * @return PMD_SUCCESS or error code + */ +static pmd_status check_nested_iteration_padding(hid_t loc_id, const char *name, const char *rest, + int64_t iteration, unsigned int padding) { + char path[PMD_PATH_MAX]; + char segment[PMD_PATH_MAX]; + nested_padding_probe probe; + + if (!name || !rest) { + return PMD_ERROR_NULL_POINTER; + } + + int written = snprintf(path, sizeof(path), "%s", name); + if (written < 0 || (size_t)written >= sizeof(path)) { + return PMD_ERROR; + } + + const char *segment_start = rest; + while (*segment_start) { + /* Skip path separators between segments */ + if (*segment_start == '/' || *segment_start == '\\') { + segment_start++; + continue; + } + + const char *segment_end = segment_start; + while (*segment_end != '\0' && *segment_end != '/' && *segment_end != '\\') { + segment_end++; + } + + size_t segment_len = segment_end - segment_start; + if (segment_len >= sizeof(segment)) { + return PMD_ERROR; + } + memcpy(segment, segment_start, segment_len); + segment[segment_len] = '\0'; + segment_start = segment_end; + + /* Descend into the name this segment is expected to have */ + char *expected = replace_iteration(segment, iteration, padding); + if (!expected) { + return PMD_ERROR_OUT_OF_MEMORY; + } + char child_path[PMD_PATH_MAX]; + written = snprintf(child_path, sizeof(child_path), "%s/%s", path, expected); + free(expected); + if (written < 0 || (size_t)written >= sizeof(child_path)) { + return PMD_ERROR; + } + + if (H5Lexists(loc_id, child_path, H5P_DEFAULT) > 0) { + memcpy(path, child_path, strlen(child_path) + 1); + continue; + } + + /* Nothing more to compare for a segment without %T */ + if (!strstr(segment, "%T")) { + return PMD_SUCCESS; + } + + /* The expected name is missing - a sibling holding the same iteration is padded differently */ + hid_t group_id = H5Gopen(loc_id, path, H5P_DEFAULT); + if (group_id < 0) { + return PMD_ERROR_HDF5; + } + + probe.segment = segment; + probe.iteration = iteration; + probe.mismatch = 0; + + herr_t iter_result = H5Literate(group_id, H5_INDEX_NAME, H5_ITER_INC, NULL, + nested_iteration_padding_callback, &probe); + H5Gclose(group_id); + + if (probe.mismatch) { + pmd_log(PMD_LOG_ERROR, "Groups below '%s' pad %%T to a different width than '%s'", path, name); + return PMD_ERROR_FILE_FORMAT; + } + if (iter_result < 0) { + return PMD_ERROR_HDF5; + } + + return PMD_SUCCESS; + } + + return PMD_SUCCESS; +} + +typedef struct { + const char *first_segment; /* First path segment of the pattern, e.g. "%T" */ + const char *rest; /* Part of the pattern after first_segment, e.g. "/step_%T/" */ + iteration_group_match match; /* Latest matching name, or the first one that is zero padded */ + pmd_status status; /* Error status from matching the names */ +} iteration_padding_probe; + +/** + * Callback for H5Literate that reports a matching name, preferring one that shows the zero padding + */ +static herr_t maybe_padded_iteration_callback(hid_t loc_id, const char *name, + const H5L_info_t *info, void *op_data) { + iteration_padding_probe *probe = (iteration_padding_probe *)op_data; + iteration_group_match match; + + (void)loc_id; + (void)info; + + /* Match into a local, since a later name that doesn't match would clear the kept padding */ + pmd_status status = extract_iteration_from_name(name, probe->first_segment, &match.iteration, + &match.padding); + if (status == PMD_ERROR_FILE_FORMAT) { + probe->status = status; + return -1; /* Group name pads its %T inconsistently */ + } + if (status != PMD_SUCCESS) { + return 0; /* Name doesn't match pattern, keep looking */ + } + + /* The groups below the match have to use the same padding */ + status = check_nested_iteration_padding(loc_id, name, probe->rest, match.iteration, match.padding); + if (status != PMD_SUCCESS) { + probe->status = status; + return -1; + } + + /* Keep the latest match, and stop once one of them shows the padding */ + match.found = 1; + probe->match = match; + if (match.padding != 0) { + return 1; + } + + return 0; /* Keep looking for a name that is wider than its iteration needs */ +} + +/** + * Detect the zero padding applied to %T by the iteration groups of an open file + * + * Scans the group holding the iterations (e.g. "/data" for a base path of "/data/%T/") and reports + * the first name that is wider than its iteration needs, since only such a name shows the padding. + * Falls back to the last matching name when none of them is padded, so the reported padding is 0 + * both when no name matches and when the names are unpadded, and match_out->found tells the two + * apart. GROUP_BASED series take their padding from here, and FILE_BASED series use it on the + * iteration groups inside one of their files. + */ +static pmd_status detect_group_based_padding(hid_t file_id, const char *pattern, + iteration_group_match *match_out) { + iteration_pattern pattern_info; + iteration_padding_probe probe; + + if (!pattern || !match_out) { + return PMD_ERROR_NULL_POINTER; + } + + match_out->found = 0; + match_out->iteration = 0; + match_out->padding = 0; + + pmd_status status = parse_iteration_pattern(pattern, &pattern_info); + if (status != PMD_SUCCESS) { + return status; + } + + /* No iterations stored yet - nothing to detect */ + if (record_exists(file_id, pattern_info.scan_parent) < 1) { + free_iteration_pattern(&pattern_info); + return PMD_SUCCESS; + } + + hid_t group_id = H5Gopen(file_id, pattern_info.scan_parent, H5P_DEFAULT); + if (group_id < 0) { + free_iteration_pattern(&pattern_info); + return PMD_ERROR_HDF5; + } + + probe.first_segment = pattern_info.first_segment; + probe.rest = pattern_info.rest; + probe.match = *match_out; + probe.status = PMD_SUCCESS; + + herr_t iter_result = H5Literate(group_id, H5_INDEX_NAME, H5_ITER_INC, NULL, + maybe_padded_iteration_callback, &probe); + + H5Gclose(group_id); + free_iteration_pattern(&pattern_info); + + if (probe.status != PMD_SUCCESS) { + return probe.status; + } + if (iter_result < 0) { + return PMD_ERROR_HDF5; + } + + *match_out = probe.match; + return PMD_SUCCESS; +} + + /** * Read series metadata from an existing HDF5 file * @@ -1715,6 +2000,28 @@ static void normalize_path_separators(char *path) { } } +/** + * Skip a leading directory prefix on a path + * Returns a pointer into path just past directory and its separator, or path unchanged when + * directory is "." or is not a prefix of path + */ +static const char* path_after_directory(const char *path, const char *directory) { + if (strcmp(directory, ".") == 0) { + return path; + } + + size_t dir_len = strlen(directory); + if (strncmp(path, directory, dir_len) != 0) { + return path; + } + + const char *rest = path + dir_len; + if (*rest == '/' || *rest == '\\') { + rest++; + } + return rest; +} + /** * Delete all iteration files matching the pattern * Used during truncate mode to remove existing iterations @@ -1738,11 +2045,12 @@ static pmd_status delete_matching_iteration_files(const char *pattern) { pmd_dirent *del_entry; while ((del_entry = pmd_readdir(del_dir)) != NULL) { int64_t iter_index; - if (extract_iteration_from_name(del_entry->d_name, pattern_info.first_segment, &iter_index) == PMD_SUCCESS) { + unsigned int iter_padding; + if (extract_iteration_from_name(del_entry->d_name, pattern_info.first_segment, &iter_index, &iter_padding) == PMD_SUCCESS) { /* Reconstruct full file path from pattern (for paths like data_%T/file_%T.h5 * where we only found the directory data_1) */ free(full_path); - full_path = replace_iteration(pattern, iter_index); + full_path = replace_iteration(pattern, iter_index, iter_padding); if (!full_path) { pmd_log(PMD_LOG_ERROR, "delete_matching_iteration_files - out of memory constructing path."); status = PMD_ERROR_OUT_OF_MEMORY; @@ -1766,17 +2074,166 @@ static pmd_status delete_matching_iteration_files(const char *pattern) { return status; } +/** + * Find an existing file matching an iteration pattern, preferring one that reveals the zero padding + * + * Only a name that is wider than its iteration needs (e.g. "data_0001.h5") shows the padding of a + * series, so the scan keeps going until it finds one. Sets *path_out to that file, or to the last + * matching file in directory order when none of them is padded (caller must free). Sets *path_out to + * NULL when nothing matches on disk. + */ +static pmd_status find_maybe_padded_iteration_file(const char *filename, const iteration_pattern *pattern_info, + char **path_out) { + pmd_dirent *entry; + + *path_out = NULL; + + /* Open directory and search for matching files */ + pmd_dir *dir = pmd_opendir(pattern_info->scan_parent); + if (!dir) { + return PMD_ERROR_FILE_NOT_FOUND; + } + + while ((entry = pmd_readdir(dir)) != NULL) { + int64_t iteration; + unsigned int padding; + + /* Try to extract iteration from name matching first segment pattern */ + pmd_status match_status = extract_iteration_from_name(entry->d_name, pattern_info->first_segment, + &iteration, &padding); + if (match_status == PMD_ERROR_FILE_FORMAT) { + /* File name pads its %T inconsistently */ + free(*path_out); + *path_out = NULL; + pmd_closedir(dir); + return match_status; + } + if (match_status != PMD_SUCCESS) { + continue; + } + + /* Reconstruct full file path from pattern (ie for paths that look like data_%T/file_%T.h5 where we only found + * the directory data_1). The padding of the matched name has to be carried over or a zero padded + * name on disk rebuilds to a path that does not exist. */ + char *full_path = replace_iteration(filename, iteration, padding); + if (!full_path) { + pmd_log(PMD_LOG_ERROR, "find_maybe_padded_iteration_file - out of memory constructing path."); + free(*path_out); + *path_out = NULL; + pmd_closedir(dir); + return PMD_ERROR_OUT_OF_MEMORY; + } + + /* Test for file existance */ + FILE *test = fopen(full_path, "rb"); + if (!test) { + free(full_path); + continue; + } + (void)fclose(test); + + /* Keep the latest match, and stop once one of them shows the padding */ + free(*path_out); + *path_out = full_path; + if (padding != 0) { + break; + } + } + + pmd_closedir(dir); + return PMD_SUCCESS; +} + +/** + * Detect the zero padding applied to %T by an existing FILE_BASED series + * + * The name of an existing file of the series (relative to directory) has to match iteration_format. + * Its padding is the padding of the series, and the iteration group inside the file has to agree + * with it. + * + * @param maybe_padded_file Path of an existing file of the series, zero padded if any file is + * (see find_maybe_padded_iteration_file) + * @param directory Directory holding the files of the series + * @param iteration_format Iteration format of the series, e.g. "data_%T.h5" + * @param base_path Base path holding the iteration groups, e.g. "/data/%T/" + * @param padding_out Output for the detected padding width + * @return PMD_SUCCESS or error code + */ +static pmd_status detect_file_based_padding(const char *maybe_padded_file, const char *directory, + const char *iteration_format, const char *base_path, + unsigned int *padding_out) { + pmd_status status; + iteration_group_match group_match; + + if (!maybe_padded_file || !directory || !iteration_format || !base_path || !padding_out) { + return PMD_ERROR_NULL_POINTER; + } + + *padding_out = 0; + + /* Match the file name against iteration_format to get its padding */ + const char *relative_path = path_after_directory(maybe_padded_file, directory); + while (*relative_path == '/' || *relative_path == '\\') { + relative_path++; + } + char *relative_name = pmd_strdup(relative_path); + if (!relative_name) { + return PMD_ERROR_OUT_OF_MEMORY; + } + normalize_path_separators(relative_name); + + int64_t file_iteration; + unsigned int file_padding = 0; + status = extract_iteration_from_name(relative_name, iteration_format, &file_iteration, &file_padding); + if (status == PMD_ERROR_FILE_FORMAT) { + /* File name pads its %T inconsistently, already logged */ + free(relative_name); + return status; + } + if (status != PMD_SUCCESS) { + pmd_log(PMD_LOG_ERROR, "File name '%s' does not match iterationFormat '%s'", + relative_name, iteration_format); + free(relative_name); + return PMD_ERROR_FILE_FORMAT; + } + free(relative_name); + *padding_out = file_padding; + + /* The iteration group inside the file (e.g. "/data/00001/") has to agree with the file name */ + hid_t maybe_padded_file_id = H5Fopen(maybe_padded_file, H5F_ACC_RDONLY, H5P_DEFAULT); + if (maybe_padded_file_id < 0) { + pmd_log(PMD_LOG_ERROR, "Failed to open '%s' to detect its iteration groups", maybe_padded_file); + return PMD_ERROR_HDF5; + } + status = detect_group_based_padding(maybe_padded_file_id, base_path, &group_match); + H5Fclose(maybe_padded_file_id); + if (status != PMD_SUCCESS) { + return status; + } + + if (group_match.found && group_match.iteration == file_iteration && + group_match.padding != file_padding) { + pmd_log(PMD_LOG_ERROR, "Iteration group of '%s' pads %%T to a different width than its file name", + maybe_padded_file); + return PMD_ERROR_FILE_FORMAT; + } + + return PMD_SUCCESS; +} + pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_access_mode mode) { pmd_series *series = NULL; hid_t file_id = -1; pmd_status status = PMD_SUCCESS; int is_write_mode = (mode != PMD_RDONLY); int file_exists = 0; - char *iter_filename = NULL; + char *maybe_padded_iter_filename = NULL; + const char *maybe_padded_file = NULL; iteration_pattern pattern_info; pattern_info.scan_parent = NULL; pattern_info.first_segment = NULL; pattern_info.full_pattern = NULL; + pattern_info.rest = NULL; /* Validate input */ if (!filename || !series_out) { @@ -1808,6 +2265,7 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac series->_meshes_path = NULL; series->directory = NULL; series->iteration_format = NULL; + series->iteration_padding = 0; series->base_path = NULL; series->_particles_path = NULL; series->_author = NULL; @@ -1824,46 +2282,11 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac status = parse_iteration_pattern(filename, &pattern_info); if (status != PMD_SUCCESS) goto cleanup; - /* Open directory and search for matching files */ - pmd_dir *dir = pmd_opendir(pattern_info.scan_parent); - if (!dir) { - status = PMD_ERROR_FILE_NOT_FOUND; - goto cleanup; - } - /* Find first file matching the pattern */ - int found = 0; - int64_t first_iteration; - pmd_dirent *entry; - if (dir) { - while ((entry = pmd_readdir(dir)) != NULL && !found) { - /* Try to extract iteration from name matching first segment pattern */ - if (extract_iteration_from_name(entry->d_name, pattern_info.first_segment, &first_iteration) == PMD_SUCCESS) { - /* Reconstruct full file path from pattern (ie for paths that look like data_%T/file_%T.h5 where we only found - * the directory data_1) */ - char *full_path = replace_iteration(filename, first_iteration); - if (!full_path) { - pmd_closedir(dir); - free_iteration_pattern(&pattern_info); - free(series); - return PMD_ERROR_OUT_OF_MEMORY; - } - - /* Test for file existance */ - FILE *test = fopen(full_path, "rb"); - if (test) { - found = 1; - free(full_path); - (void)fclose(test); - break; - } - free(full_path); - } - } - pmd_closedir(dir); - } + status = find_maybe_padded_iteration_file(filename, &pattern_info, &maybe_padded_iter_filename); + if (status != PMD_SUCCESS) goto cleanup; - if (!found) { + if (!maybe_padded_iter_filename) { /* No existing files found */ if (!is_write_mode) { status = PMD_ERROR_FILE_NOT_FOUND; @@ -1875,18 +2298,7 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac series->iteration_encoding = PMD_FILE_BASED; /* Extract filename pattern (everything after directory) */ - const char *filename_pattern = filename; - if (strcmp(pattern_info.scan_parent, ".") != 0) { - /* Skip past directory and separator */ - size_t dir_len = strlen(pattern_info.scan_parent); - if (strncmp(filename, pattern_info.scan_parent, dir_len) == 0) { - filename_pattern = filename + dir_len; - /* Skip separator */ - if (*filename_pattern == '/' || *filename_pattern == '\\') { - filename_pattern++; - } - } - } + const char *filename_pattern = path_after_directory(filename, pattern_info.scan_parent); series->iteration_format = pmd_strdup(filename_pattern); normalize_path_separators(series->iteration_format); series->base_path = pmd_strdup("/data/%T/"); @@ -1913,18 +2325,7 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac series->iteration_encoding = PMD_FILE_BASED; /* Extract filename pattern (everything after directory) */ - const char *filename_pattern = filename; - if (strcmp(pattern_info.scan_parent, ".") != 0) { - /* Skip past directory and separator */ - size_t dir_len = strlen(pattern_info.scan_parent); - if (strncmp(filename, pattern_info.scan_parent, dir_len) == 0) { - filename_pattern = filename + dir_len; - /* Skip separator */ - if (*filename_pattern == '/' || *filename_pattern == '\\') { - filename_pattern++; - } - } - } + const char *filename_pattern = path_after_directory(filename, pattern_info.scan_parent); series->iteration_format = pmd_strdup(filename_pattern); normalize_path_separators(series->iteration_format); series->base_path = pmd_strdup("/data/%T/"); @@ -1933,21 +2334,15 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac /* Don't create any files yet - will be created when iterations are added */ series->file_id = -1; } else { - iter_filename = replace_iteration(filename, first_iteration); - if (!iter_filename) { - pmd_closedir(dir); - status = PMD_ERROR_OUT_OF_MEMORY; - goto cleanup; - } - // Open the HDF5 file - file_id = H5Fopen(iter_filename, H5F_ACC_RDONLY, H5P_DEFAULT); + file_id = H5Fopen(maybe_padded_iter_filename, H5F_ACC_RDONLY, H5P_DEFAULT); if (file_id >= 0) { /* Read metadata from the opened file */ status = read_series_metadata_from_file(file_id, series, filename); if (status != PMD_SUCCESS) { goto cleanup; } + maybe_padded_file = maybe_padded_iter_filename; /* Close file since we will not store for file-based mode */ H5Fclose(file_id); @@ -1986,27 +2381,7 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac if (status != PMD_SUCCESS) goto cleanup; /* Create base path structure up to scan_parent for GROUP_BASED */ - /* This ensures pmd_list_iterations can enumerate groups even when no iterations exist yet */ - iteration_pattern pattern_info; - status = parse_iteration_pattern(series->base_path, &pattern_info); - if (status == PMD_SUCCESS) { - /* Create the scan_parent group if it doesn't exist (e.g., "/data" for "/data/%T/") */ - if (strcmp(pattern_info.scan_parent, ".") != 0) { - status = ensure_parent_groups(file_id, pattern_info.scan_parent); - if (status == PMD_SUCCESS) { - /* Create the scan_parent group itself */ - htri_t exists = H5Lexists(file_id, pattern_info.scan_parent, H5P_DEFAULT); - if (exists == 0) { - hid_t group_id = H5Gcreate(file_id, pattern_info.scan_parent, - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - if (group_id >= 0) { - H5Gclose(group_id); - } - } - } - } - free_iteration_pattern(&pattern_info); - } + status = ensure_base_path_groups(file_id, series->base_path); /* Keep file open for group-based */ series->file_id = file_id; @@ -2043,42 +2418,28 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac if (status != PMD_SUCCESS) { goto cleanup; } + maybe_padded_file = filename; } /* For FILE_BASED series, set directory to parent of filename */ if (series->iteration_encoding == PMD_FILE_BASED) { series->directory = pmd_dirname(filename); + } + } - if (file_exists){ - /* Validate that iterationFormat matches the actual filename */ - /* Extract just the filename from the full path */ - const char *actual_basename = strrchr(filename, '/'); - if (!actual_basename) { - actual_basename = strrchr(filename, '\\'); - } - if (actual_basename) { - actual_basename++; /* Skip past the separator */ - } else { - actual_basename = filename; /* No path separator, use whole string */ - } - - /* Try to extract iteration from actual filename using the iterationFormat pattern */ - int64_t extracted_iteration; - iteration_pattern actual_pattern; - pmd_status match_status = parse_iteration_pattern(series->iteration_format, &actual_pattern); - if (match_status == PMD_SUCCESS) { - pmd_status extract_status = extract_iteration_from_name(actual_basename, - actual_pattern.first_segment, - &extracted_iteration); - free_iteration_pattern(&actual_pattern); - if (extract_status != PMD_SUCCESS) { - /* Filename doesn't match the pattern specified in iterationFormat */ - status = PMD_ERROR_FILE_FORMAT; - goto cleanup; - } - } + /* Detect zero padded %T from the names of an existing series */ + if (maybe_padded_file) { + if (series->iteration_encoding == PMD_GROUP_BASED) { + iteration_group_match group_match; + status = detect_group_based_padding(series->file_id, series->base_path, &group_match); + if (status == PMD_SUCCESS) { + series->iteration_padding = group_match.padding; } + } else { + status = detect_file_based_padding(maybe_padded_file, series->directory, series->iteration_format, + series->base_path, &series->iteration_padding); } + if (status != PMD_SUCCESS) goto cleanup; } /* Write root attributes if in write mode */ @@ -2095,7 +2456,7 @@ pmd_status pmd_open_series(const char *filename, pmd_series **series_out, pmd_ac pmd_close_series(series); series = NULL; } - free(iter_filename); + free(maybe_padded_iter_filename); free_iteration_pattern(&pattern_info); *series_out = series; return status; @@ -2158,7 +2519,9 @@ typedef struct { int capacity; const char *first_segment; /* Pattern for first segment (e.g., "data_%T") */ const char *full_pattern; /* Full pattern for validation (e.g., "/data/%T/step_%T/") */ + const char *rest; /* Part of full_pattern after first_segment (e.g., "/step_%T/") */ hid_t root_id; /* Root file ID for validating full paths */ + unsigned int padding; /* Zero padding width applied when expanding %T */ pmd_status status; /* Error status from memory allocation */ } iteration_collector; @@ -2171,14 +2534,37 @@ static herr_t collect_iterations_callback(hid_t loc_id, const char *name, int64_t iteration; /* Try to extract iteration number from name matching first segment pattern */ - if (extract_iteration_from_name(name, collector->first_segment, &iteration) != PMD_SUCCESS) { + pmd_status match_status = extract_iteration_from_name(name, collector->first_segment, &iteration, NULL); + if (match_status == PMD_ERROR_FILE_FORMAT) { + /* Group name pads its %T inconsistently */ + collector->status = match_status; + return -1; + } + if (match_status != PMD_SUCCESS) { return 0; /* Name doesn't match pattern, skip */ } + /* Every iteration of a series is padded the same way */ + char *expected_name = replace_iteration(collector->first_segment, iteration, collector->padding); + if (!expected_name) { + collector->status = PMD_ERROR_OUT_OF_MEMORY; + return -1; + } + int name_differs = strcmp(expected_name, name) != 0; + if (name_differs) { + pmd_log(PMD_LOG_ERROR, "Iteration group '%s' is not padded like the rest of the series (expected '%s')", + name, expected_name); + } + free(expected_name); + if (name_differs) { + collector->status = PMD_ERROR_FILE_FORMAT; + return -1; + } + /* Validate full path if pattern has additional components after first segment */ if (collector->full_pattern) { /* Replace all %T in full pattern with extracted iteration */ - char *full_path = replace_iteration(collector->full_pattern, iteration); + char *full_path = replace_iteration(collector->full_pattern, iteration, collector->padding); if (!full_path) { collector->status = PMD_ERROR_OUT_OF_MEMORY; return -1; @@ -2189,6 +2575,14 @@ static herr_t collect_iterations_callback(hid_t loc_id, const char *name, free(full_path); if (exists <= 0) { + /* The expected path is missing - the groups below may just be padded differently */ + pmd_status nested_status = check_nested_iteration_padding(loc_id, name, collector->rest, + iteration, collector->padding); + if (nested_status != PMD_SUCCESS) { + collector->status = nested_status; + return -1; + } + /* Path doesn't exist or error checking - skip this iteration */ return 0; } @@ -2221,6 +2615,7 @@ static pmd_status pmd_parse_iterations(pmd_series *series) { pattern_info.scan_parent = NULL; pattern_info.first_segment = NULL; pattern_info.full_pattern = NULL; + pattern_info.rest = NULL; /* Check for single-snapshot file (no %T in iteration_format) */ if (!strstr(series->iteration_format, "%T")) { @@ -2238,11 +2633,13 @@ static pmd_status pmd_parse_iterations(pmd_series *series) { if (status != PMD_SUCCESS) goto cleanup; /* Common collector for both GROUP_BASED and FILE_BASED */ - iteration_collector collector = {NULL, 0, 0, NULL, NULL, -1, PMD_SUCCESS}; + iteration_collector collector = {NULL, 0, 0, NULL, NULL, NULL, -1, 0, PMD_SUCCESS}; /* Initialize collector with pattern info */ collector.first_segment = pattern_info.first_segment; collector.full_pattern = pattern_info.full_pattern; + collector.rest = pattern_info.rest; + collector.padding = series->iteration_padding; if (series->iteration_encoding == PMD_GROUP_BASED) { /* GROUP_BASED: enumerate groups using pattern matching */ @@ -2289,16 +2686,41 @@ static pmd_status pmd_parse_iterations(pmd_series *series) { int64_t iteration; /* Try to extract iteration from name matching first segment pattern */ - if (extract_iteration_from_name(entry->d_name, pattern_info.first_segment, &iteration) != PMD_SUCCESS) { + pmd_status match_status = extract_iteration_from_name(entry->d_name, pattern_info.first_segment, + &iteration, NULL); + if (match_status == PMD_ERROR_FILE_FORMAT) { + /* File name pads its %T inconsistently */ + collector.status = match_status; + break; + } + if (match_status != PMD_SUCCESS) { continue; /* Name doesn't match, skip */ } + /* Every iteration of a series is padded the same way */ + char *expected_name = replace_iteration(pattern_info.first_segment, iteration, + series->iteration_padding); + if (!expected_name) { + collector.status = PMD_ERROR_OUT_OF_MEMORY; + break; + } + int name_differs = strcmp(expected_name, entry->d_name) != 0; + if (name_differs) { + pmd_log(PMD_LOG_ERROR, "File name '%s' is not padded like the rest of the series (expected '%s')", + entry->d_name, expected_name); + } + free(expected_name); + if (name_differs) { + collector.status = PMD_ERROR_FILE_FORMAT; + break; + } + /* Validate full path if pattern has additional path components */ if (pattern_info.full_pattern && (strchr(pattern_info.full_pattern, '/') || strchr(pattern_info.full_pattern, '\\'))) { int sstatus; /* Build full file path with iteration substituted */ - char *rel_path = replace_iteration(series->iteration_format, iteration); + char *rel_path = replace_iteration(series->iteration_format, iteration, series->iteration_padding); if (!rel_path) { collector.status = PMD_ERROR_OUT_OF_MEMORY; break; @@ -2458,6 +2880,7 @@ static pmd_status parse_iteration_pattern(const char *pattern, iteration_pattern info->scan_parent = NULL; info->first_segment = NULL; info->full_pattern = pattern; + info->rest = pattern + strlen(pattern); /* Find first %T */ const char *first_format_char = strstr(pattern, "%T"); @@ -2511,6 +2934,7 @@ static pmd_status parse_iteration_pattern(const char *pattern, iteration_pattern } strncpy(info->first_segment, segment_start, segment_len); info->first_segment[segment_len] = '\0'; + info->rest = segment_end; return PMD_SUCCESS; } @@ -2525,31 +2949,54 @@ static void free_iteration_pattern(iteration_pattern *info) { info->scan_parent = NULL; info->first_segment = NULL; info->full_pattern = NULL; + info->rest = NULL; + } +} + +/** + * Compare two digit strings by value, ignoring leading zeros + */ +static int digit_strings_equal_value(const char *a, size_t a_len, const char *b, size_t b_len) { + while (a_len > 1 && *a == '0') { + a++; + a_len--; + } + while (b_len > 1 && *b == '0') { + b++; + b_len--; } + return a_len == b_len && strncmp(a, b, a_len) == 0; } /** * Extract iteration number from a name matching a pattern segment * * Matches names like "data_5" against patterns like "data_%T" and extracts 5. - * Handles multiple %T in the pattern (all must match the same number). + * Handles multiple %T in the pattern (all must match the same number, padded the same way). * * @param name Name to match (e.g., "data_5") * @param pattern Pattern to match against (e.g., "data_%T") * @param iteration_out Output for extracted iteration number - * @return PMD_SUCCESS if match found and iteration extracted, error otherwise + * @param padding_out Output for the zero padding width of the match, 0 when unpadded (may be NULL) + * @return PMD_SUCCESS if match found and iteration extracted, PMD_ERROR_FILE_FORMAT if the name + * matches but pads its %T to different widths, PMD_ERROR if the name doesn't match */ static pmd_status extract_iteration_from_name(const char *name, const char *pattern, - int64_t *iteration_out) { + int64_t *iteration_out, unsigned int *padding_out) { const char *p = pattern; const char *n = name; char matched_number[64] = {0}; int found_format_char = 0; + int padding_mismatch = 0; if (!name || !pattern || !iteration_out) { return PMD_ERROR_NULL_POINTER; } + if (padding_out) { + *padding_out = 0; + } + /* Check for ambiguous consecutive %T patterns */ const char *check = pattern; while (*check) { @@ -2587,10 +3034,15 @@ static pmd_status extract_iteration_from_name(const char *name, const char *patt matched_number[digit_len] = '\0'; found_format_char = 1; } else { - /* Subsequent %T must match same digits */ + /* Subsequent %T must match the same digits, zero padding included */ if (strlen(matched_number) != digit_len || strncmp(matched_number, digit_start, digit_len) != 0) { - return PMD_ERROR; + if (!digit_strings_equal_value(matched_number, strlen(matched_number), + digit_start, digit_len)) { + return PMD_ERROR; + } + /* Same iteration, different zero padding - only an error if the rest matches */ + padding_mismatch = 1; } } @@ -2619,6 +3071,20 @@ static pmd_status extract_iteration_from_name(const char *name, const char *patt return PMD_ERROR; } + if (padding_mismatch) { + pmd_log(PMD_LOG_ERROR, "'%s' matches '%s' but pads its %%T to different widths", name, pattern); + return PMD_ERROR_FILE_FORMAT; + } + + /* Report the zero padding width, if the name was wider than the bare number needs */ + if (padding_out) { + char unpadded[64]; + int unpadded_len = snprintf(unpadded, sizeof(unpadded), "%lld", (long long)*iteration_out); + if (unpadded_len > 0 && (size_t)unpadded_len < strlen(matched_number)) { + *padding_out = (unsigned int)strlen(matched_number); + } + } + return PMD_SUCCESS; } @@ -2637,7 +3103,7 @@ static pmd_status extract_iteration_from_name(const char *name, const char *patt * @param iteration Iteration number to substitute * @return Newly allocated string with all %T replaced (caller must free), or NULL on error */ -static char* replace_iteration(const char *pattern, int64_t iteration) { +static char* replace_iteration(const char *pattern, int64_t iteration, unsigned int padding) { if (!pattern) { return NULL; } @@ -2655,9 +3121,14 @@ static char* replace_iteration(const char *pattern, int64_t iteration) { return pmd_strdup(pattern); } - /* Format iteration number */ - char iter_str[32]; - int status = snprintf(iter_str, sizeof(iter_str), "%lld", (long long)iteration); + /* Format iteration number, zero padded to the requested width (0 means no minimum width) */ + char iter_str[64]; + if (padding >= sizeof(iter_str)) { + pmd_log(PMD_LOG_ERROR, "replace_iteration - Padding width %u exceeds maximum of %zu.", + padding, sizeof(iter_str) - 1); + return NULL; + } + int status = snprintf(iter_str, sizeof(iter_str), "%0*lld", (int)padding, (long long)iteration); if (status < 0) { pmd_log(PMD_LOG_ERROR, "replace_iteration - Failed to format iteration count into string."); return NULL; @@ -2863,7 +3334,7 @@ pmd_status pmd_open_iteration(pmd_series *series, int64_t index, pmd_iteration * iter->iteration_group_id = -1; /* Construct iteration group path */ - iteration_path = replace_iteration(series->base_path, index); + iteration_path = replace_iteration(series->base_path, index, series->iteration_padding); if (!iteration_path) { status = PMD_ERROR_OUT_OF_MEMORY; goto cleanup; @@ -2940,7 +3411,7 @@ pmd_status pmd_open_iteration(pmd_series *series, int64_t index, pmd_iteration * } } else { /* Not already open - open or create file for this iteration */ - filename = replace_iteration(series->iteration_format, index); + filename = replace_iteration(series->iteration_format, index, series->iteration_padding); if (!filename) { status = PMD_ERROR_OUT_OF_MEMORY; goto cleanup; diff --git a/python/generate_test_data.py b/python/generate_test_data.py index a0a10a2..fa7c994 100644 --- a/python/generate_test_data.py +++ b/python/generate_test_data.py @@ -1569,6 +1569,145 @@ def make_valid_file_based_series(dirname: str): print(f"make_valid_file_based_series: Created 3 files in {dirname}") +def make_zero_padded_file_based_series(dirname: str): + """File-based series whose filenames and iteration groups are zero padded""" + dir_path = Path(dirname) + dir_path.mkdir(parents=True, exist_ok=True) + + # Create 3 files: data_0001.h5, data_0002.h5, data_0003.h5 + for iteration in [1, 2, 3]: + fname = dir_path / f"data_{iteration:04d}.h5" + with h5py.File(fname, "w") as f: + write_openpmd_header( + f, base_path="/data/%T/", iteration_encoding="fileBased" + ) + f.attrs["iterationFormat"] = "data_%T.h5" + + iter_grp = f.create_group(f"data/{iteration:04d}") + write_iteration_attributes(iter_grp, time=iteration * 1.0e-15) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_zero_padded_file_based_series: Created 3 files in {dirname}") + + +def make_partially_padded_file_based_series(dirname: str): + """File-based series where only some iterations are wide enough to show the padding""" + dir_path = Path(dirname) + dir_path.mkdir(parents=True, exist_ok=True) + + # Only data_0001.h5 shows the padding, the others are four digits on their own + for iteration in [1] + list(range(1000, 1010)): + fname = dir_path / f"data_{iteration:04d}.h5" + with h5py.File(fname, "w") as f: + write_openpmd_header( + f, base_path="/data/%T/", iteration_encoding="fileBased" + ) + f.attrs["iterationFormat"] = "data_%T.h5" + + iter_grp = f.create_group(f"data/{iteration:04d}") + write_iteration_attributes(iter_grp, time=iteration * 1.0e-15) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_partially_padded_file_based_series: Created 11 files in {dirname}") + + +def make_partially_padded_group_based(fname: str): + """Group-based series where only one iteration group is wide enough to show the padding""" + with h5py.File(fname, "w") as f: + write_openpmd_header(f, base_path="/data/%T/", iteration_encoding="groupBased") + + for iteration in [1] + list(range(1000, 1010)): + iter_grp = f.create_group(f"data/{iteration:04d}") + write_iteration_attributes(iter_grp, time=iteration * 1.0e-15) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_partially_padded_group_based: Created {fname}") + + +def make_mixed_padding_file_based_series(dirname: str): + """File-based series where one iteration is zero padded and another is not""" + dir_path = Path(dirname) + dir_path.mkdir(parents=True, exist_ok=True) + + # Each file agrees with itself, only the two names disagree with each other + for name in ["0001", "10"]: + fname = dir_path / f"data_{name}.h5" + with h5py.File(fname, "w") as f: + write_openpmd_header( + f, base_path="/data/%T/", iteration_encoding="fileBased" + ) + f.attrs["iterationFormat"] = "data_%T.h5" + + iter_grp = f.create_group(f"data/{name}") + write_iteration_attributes(iter_grp) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_mixed_padding_file_based_series: Created 2 files in {dirname}") + + +def make_mixed_padding_group_based(fname: str): + """Group-based series where one iteration group is zero padded and another is not""" + with h5py.File(fname, "w") as f: + write_openpmd_header(f, base_path="/data/%T/", iteration_encoding="groupBased") + + for name in ["0001", "10"]: + iter_grp = f.create_group(f"data/{name}") + write_iteration_attributes(iter_grp) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_mixed_padding_group_based: Created {fname}") + + +def make_padding_mismatch_file_based_multiple_percent_t(dirname: str): + """File-based series whose filename pads each %T to a different width""" + dir_path = Path(dirname) + dir_path.mkdir(parents=True, exist_ok=True) + + fname = dir_path / "data_0001_iter_1.h5" + with h5py.File(fname, "w") as f: + write_openpmd_header(f, base_path="/data/%T/", iteration_encoding="fileBased") + f.attrs["iterationFormat"] = "data_%T_iter_%T.h5" + + iter_grp = f.create_group("data/0001") + write_iteration_attributes(iter_grp) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_padding_mismatch_file_based_multiple_percent_t: Created {fname}") + + +def make_padding_mismatch_group_based(fname: str, iteration_group: str): + """Group-based series with %T in two groups padded to different widths""" + with h5py.File(fname, "w") as f: + write_openpmd_header( + f, base_path="/data/%T/step_%T/", iteration_encoding="groupBased" + ) + + iter_grp = f.create_group(iteration_group) + write_iteration_attributes(iter_grp) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_padding_mismatch_group_based: Created {fname}") + + +def make_padding_mismatch_file_based_root_group( + dirname: str, file_name: str, iteration_group: str +): + """File-based series whose filename and iteration group pad %T differently""" + dir_path = Path(dirname) + dir_path.mkdir(parents=True, exist_ok=True) + + fname = dir_path / file_name + with h5py.File(fname, "w") as f: + write_openpmd_header(f, base_path="/data/%T/", iteration_encoding="fileBased") + f.attrs["iterationFormat"] = "data_%T.h5" + + iter_grp = f.create_group(iteration_group) + write_iteration_attributes(iter_grp) + write_particle_group(iter_grp, "electron", 10) + + print(f"make_padding_mismatch_file_based_root_group: Created {fname}") + + def make_file_based_series_with_other_files(dirname: str): """File-based series with other non-matching files in directory""" dir_path = Path(dirname) @@ -1937,6 +2076,42 @@ def make_valid_partial_optional_fields(fname: str): print("=" * 80) make_valid_file_based_series(str(test_data_dir / "file_based_series")) + make_zero_padded_file_based_series( + str(test_data_dir / "file_based_series_zero_padded") + ) + make_partially_padded_file_based_series( + str(test_data_dir / "file_based_series_partially_padded") + ) + make_partially_padded_group_based( + str(test_data_dir / "group_based_series_partially_padded.h5") + ) + make_mixed_padding_file_based_series( + str(test_data_dir / "file_based_series_mixed_padding") + ) + make_mixed_padding_group_based( + str(test_data_dir / "group_based_series_mixed_padding.h5") + ) + make_padding_mismatch_file_based_multiple_percent_t( + str(test_data_dir / "padding_mismatch_file_based_multiple_percent_t") + ) + make_padding_mismatch_group_based( + str(test_data_dir / "padding_mismatch_group_based_padded_outer.h5"), + "data/0001/step_1", + ) + make_padding_mismatch_group_based( + str(test_data_dir / "padding_mismatch_group_based_padded_inner.h5"), + "data/1/step_0001", + ) + make_padding_mismatch_file_based_root_group( + str(test_data_dir / "padding_mismatch_file_based_padded_filename"), + "data_0001.h5", + "data/1", + ) + make_padding_mismatch_file_based_root_group( + str(test_data_dir / "padding_mismatch_file_based_padded_group"), + "data_1.h5", + "data/0001", + ) make_file_based_series_with_other_files( str(test_data_dir / "file_based_series_with_other") ) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bfc5de2..d89fa6e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,9 @@ else() message(WARNING "Python3 not found - test data will not be generated automatically") endif() +# Copy checked-in reference data alongside the generated test data +configure_file(data/bmad-dump.h5 ${CMAKE_CURRENT_BINARY_DIR}/data/bmad-dump.h5 COPYONLY) + # Test executables add_executable(test_read test_read.c) add_executable(test_utilities test_utilities.c) @@ -118,28 +121,28 @@ find_program(OPENPMD_VALIDATOR openPMD_check_h5) if(OPENPMD_VALIDATOR) add_test( NAME ValidateGroupBasedOpenPMD - COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/generated_openpmd/example_group_based.h5 + COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/data/generated_openpmd/example_group_based.h5 WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) set_tests_properties(ValidateGroupBasedOpenPMD PROPERTIES DEPENDS GenerateOpenPMDTests) add_test( NAME ValidateFileBasedOpenPMD_iter0 - COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/generated_openpmd/file_based_example/data_0.h5 + COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/data/generated_openpmd/file_based_example/data_0.h5 WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) set_tests_properties(ValidateFileBasedOpenPMD_iter0 PROPERTIES DEPENDS GenerateOpenPMDTests) add_test( NAME ValidateFileBasedOpenPMD_iter5 - COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/generated_openpmd/file_based_example/data_5.h5 + COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/data/generated_openpmd/file_based_example/data_5.h5 WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) set_tests_properties(ValidateFileBasedOpenPMD_iter5 PROPERTIES DEPENDS GenerateOpenPMDTests) add_test( NAME ValidateFileBasedOpenPMD_iter10 - COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/generated_openpmd/file_based_example/data_10.h5 + COMMAND ${OPENPMD_VALIDATOR} -i ${CMAKE_CURRENT_BINARY_DIR}/data/generated_openpmd/file_based_example/data_10.h5 WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) set_tests_properties(ValidateFileBasedOpenPMD_iter10 PROPERTIES DEPENDS GenerateOpenPMDTests) diff --git a/tests/data/bmad-dump.h5 b/tests/data/bmad-dump.h5 new file mode 100644 index 0000000..bed0ff4 Binary files /dev/null and b/tests/data/bmad-dump.h5 differ diff --git a/tests/test_generate_openpmd.c b/tests/test_generate_openpmd.c index a112164..536609c 100644 --- a/tests/test_generate_openpmd.c +++ b/tests/test_generate_openpmd.c @@ -21,10 +21,12 @@ #include #endif -#define TEST_OUTPUT_DIR "tests/generated_openpmd" +#define TEST_DATA_DIR "tests/data" +#define TEST_OUTPUT_DIR TEST_DATA_DIR "/generated_openpmd" void setUp(void) { - /* Create output directory if it doesn't exist */ + /* Create output directories if they don't exist */ + mkdir(TEST_DATA_DIR, 0755); mkdir(TEST_OUTPUT_DIR, 0755); } diff --git a/tests/test_read.c b/tests/test_read.c index 096c31d..8a3c2fc 100644 --- a/tests/test_read.c +++ b/tests/test_read.c @@ -917,6 +917,277 @@ void test_valid_all_metadata(void) { pmd_close_series(series); } +/* Test: File-based series with zero padded filenames and iteration groups + * File: tests/data/file_based_series_zero_padded/data_%T.h5 + * Tests: Padding is detected from the names on disk and used to expand %T */ +void test_file_based_series_zero_padded(void) { + pmd_series *series; + pmd_iteration *iter; + pmd_status result; + int64_t *iterations; + int num_iterations; + int64_t particle_count; + + /* Opening by pattern has to scan the directory and match the padded names */ + result = pmd_open_series("tests/data/file_based_series_zero_padded/data_%T.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(series); + + /* Padding is taken from the four digit names on disk */ + TEST_ASSERT_EQUAL_INT(PMD_FILE_BASED, series->iteration_encoding); + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + + /* All three files are found despite the indices being padded */ + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT(3, num_iterations); + TEST_ASSERT_EQUAL_INT64(1, iterations[0]); + TEST_ASSERT_EQUAL_INT64(2, iterations[1]); + TEST_ASSERT_EQUAL_INT64(3, iterations[2]); + + /* The padded filename and the padded group inside it both resolve */ + result = pmd_open_iteration(series, iterations[1], &iter); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(iter); + + result = pmd_get_num_particles(iter, "electron", &particle_count); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT64(10, particle_count); + + pmd_close_iteration(iter); + free(iterations); + pmd_close_series(series); + + /* Opening one padded file by its literal name detects the same padding */ + result = pmd_open_series("tests/data/file_based_series_zero_padded/data_0002.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + pmd_close_series(series); +} + +/* Test: File-based series where only one filename is narrow enough to show the padding + * Files: tests/data/file_based_series_partially_padded/data_{0001,1000..1009}.h5 + * Tests: The padded name decides the padding, whatever order the directory is scanned in */ +void test_file_based_series_partially_padded(void) { + pmd_series *series; + pmd_iteration *iter; + pmd_status result; + int64_t *iterations; + int num_iterations; + int64_t particle_count; + + result = pmd_open_series("tests/data/file_based_series_partially_padded/data_%T.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(series); + + /* Only data_0001.h5 is wider than its iteration needs */ + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT(11, num_iterations); + TEST_ASSERT_EQUAL_INT64(1, iterations[0]); + TEST_ASSERT_EQUAL_INT64(1000, iterations[1]); + TEST_ASSERT_EQUAL_INT64(1009, iterations[10]); + + /* Iteration 1 only resolves as data_0001.h5 */ + result = pmd_open_iteration(series, 1, &iter); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + result = pmd_get_num_particles(iter, "electron", &particle_count); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT64(10, particle_count); + pmd_close_iteration(iter); + + result = pmd_open_iteration(series, 1005, &iter); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + pmd_close_iteration(iter); + + free(iterations); + pmd_close_series(series); +} + +/* Test: Group-based series where only one group is narrow enough to show the padding + * File: tests/data/group_based_series_partially_padded.h5 + * Tests: The padded group decides the padding, whatever order the groups are scanned in */ +void test_group_based_series_partially_padded(void) { + pmd_series *series; + pmd_iteration *iter; + pmd_status result; + int64_t *iterations; + int num_iterations; + int64_t particle_count; + + result = pmd_open_series("tests/data/group_based_series_partially_padded.h5", &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(series); + + /* Only /data/0001/ is wider than its iteration needs */ + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT(11, num_iterations); + TEST_ASSERT_EQUAL_INT64(1, iterations[0]); + TEST_ASSERT_EQUAL_INT64(1000, iterations[1]); + TEST_ASSERT_EQUAL_INT64(1009, iterations[10]); + + /* Iteration 1 only resolves as /data/0001/ */ + result = pmd_open_iteration(series, 1, &iter); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + result = pmd_get_num_particles(iter, "electron", &particle_count); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT64(10, particle_count); + pmd_close_iteration(iter); + + free(iterations); + pmd_close_series(series); +} + +/* Test: File-based series where one iteration is padded and another is not + * Files: tests/data/file_based_series_mixed_padding/data_{0001,10}.h5 + * Tests: Listing reports names that are not padded like the rest of the series */ +void test_mixed_padding_file_based_series(void) { + pmd_series *series; + pmd_status result; + int64_t *iterations; + int num_iterations; + + result = pmd_open_series("tests/data/file_based_series_mixed_padding/data_%T.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + + /* data_10.h5 would have to be data_0010.h5 to belong to this series */ + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + + pmd_close_series(series); +} + +/* Test: Group-based series where one iteration group is padded and another is not + * File: tests/data/group_based_series_mixed_padding.h5 + * Tests: Listing reports groups that are not padded like the rest of the series */ +void test_mixed_padding_group_based_series(void) { + pmd_series *series; + pmd_status result; + int64_t *iterations; + int num_iterations; + + result = pmd_open_series("tests/data/group_based_series_mixed_padding.h5", &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + + /* /data/10/ would have to be /data/0010/ to belong to this series */ + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + + pmd_close_series(series); +} + +/* Test: File-based series whose filename pads each %T to a different width + * File: tests/data/padding_mismatch_file_based_multiple_percent_t/data_0001_iter_1.h5 + * Tests: Inconsistent padding between %T in one filename is reported as a format error */ +void test_padding_mismatch_file_based_multiple_percent_t(void) { + pmd_series *series; + pmd_status result; + + result = pmd_open_series("tests/data/padding_mismatch_file_based_multiple_percent_t/data_%T_iter_%T.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); + + result = pmd_open_series("tests/data/padding_mismatch_file_based_multiple_percent_t/data_0001_iter_1.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); +} + +/* Test: Group-based series with %T in two groups padded to different widths + * Files: tests/data/padding_mismatch_group_based_padded_{outer,inner}.h5 + * Tests: /data/0001/step_1/ and /data/1/step_0001/ are reported as format errors */ +void test_padding_mismatch_group_based_multiple_percent_t(void) { + pmd_series *series; + pmd_status result; + + result = pmd_open_series("tests/data/padding_mismatch_group_based_padded_outer.h5", &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); + + result = pmd_open_series("tests/data/padding_mismatch_group_based_padded_inner.h5", &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); +} + +/* Test: File-based series whose filename and iteration group pad %T differently + * Files: tests/data/padding_mismatch_file_based_padded_filename/data_0001.h5 (group /data/1/) + * tests/data/padding_mismatch_file_based_padded_group/data_1.h5 (group /data/0001/) + * Tests: Opening by pattern or by name reports a format error in both directions */ +void test_padding_mismatch_file_based_root_group(void) { + pmd_series *series; + pmd_status result; + + /* Padded filename, unpadded group */ + result = pmd_open_series("tests/data/padding_mismatch_file_based_padded_filename/data_%T.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); + + result = pmd_open_series("tests/data/padding_mismatch_file_based_padded_filename/data_0001.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); + + /* Unpadded filename, padded group */ + result = pmd_open_series("tests/data/padding_mismatch_file_based_padded_group/data_%T.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); + + result = pmd_open_series("tests/data/padding_mismatch_file_based_padded_group/data_1.h5", + &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, result); + TEST_ASSERT_NULL(series); +} + +/* Test: Reference dump produced by Bmad + * File: tests/data/bmad-dump.h5 + * Tests: Reading Bmad file with a single iteration and 1000 particles */ +void test_read_bmad_dump(void) { + pmd_series *series; + pmd_iteration *iter; + pmd_status result; + int64_t *iterations; + int num_iterations; + int64_t particle_count; + + /* Open series */ + result = pmd_open_series("tests/data/bmad-dump.h5", &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(series); + + /* The dump holds exactly one iteration */ + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT(1, num_iterations); + + result = pmd_open_iteration(series, iterations[0], &iter); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(iter); + + /* The iteration holds 1000 electrons */ + result = pmd_get_num_particles(iter, "electron", &particle_count); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT64(1000, particle_count); + + /* Clean up */ + pmd_close_iteration(iter); + free(iterations); + pmd_close_series(series); +} + /* ========================================================================= * Unit Conversion (unitSI) Tests * ========================================================================= */ @@ -2735,6 +3006,15 @@ int main(void) { RUN_TEST(test_user_supplied_arrays); RUN_TEST(test_read_openpmd_constant); RUN_TEST(test_read_openpmd_dataset); + RUN_TEST(test_file_based_series_zero_padded); + RUN_TEST(test_file_based_series_partially_padded); + RUN_TEST(test_group_based_series_partially_padded); + RUN_TEST(test_mixed_padding_file_based_series); + RUN_TEST(test_mixed_padding_group_based_series); + RUN_TEST(test_padding_mismatch_file_based_multiple_percent_t); + RUN_TEST(test_padding_mismatch_group_based_multiple_percent_t); + RUN_TEST(test_padding_mismatch_file_based_root_group); + RUN_TEST(test_read_bmad_dump); /* Unit Conversion tests */ RUN_TEST(test_position_non_si_units); diff --git a/tests/test_read_write.c b/tests/test_read_write.c index 450d2ea..2885083 100644 --- a/tests/test_read_write.c +++ b/tests/test_read_write.c @@ -26,6 +26,16 @@ #define TEST_TEMP_DIR "tests/temp_read_write" +/* Helper function to test whether a path exists */ +static int path_exists(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) { + return 0; + } + (void)fclose(f); + return 1; +} + /* Helper function to recursively remove directory */ static void remove_directory(const char *path) { #ifdef _WIN32 @@ -254,6 +264,65 @@ void test_write_and_read_particle_group_file_based(void) { test_write_and_read_particle_group_helper(TEST_TEMP_DIR "/roundtrip_%T.h5"); } +/** + * Test: Padding set on a series is used when writing and detected when reading back + */ +void test_write_and_read_zero_padded_file_based(void) { + pmd_series *series; + pmd_iteration *iter; + pmd_status result; + pmd_particle_group write_pg; + int64_t *iterations; + int num_iterations; + const int64_t NUM_PARTICLES = 16; + double x[16], y[16], z[16], px[16], py[16], pz[16], weight[16]; + + for (int64_t i = 0; i < NUM_PARTICLES; i++) { + x[i] = y[i] = z[i] = (double)i; + px[i] = py[i] = pz[i] = (double)i; + weight[i] = 1.0; + } + + memset(&write_pg, 0, sizeof(pmd_particle_group)); + write_pg.num_particles = NUM_PARTICLES; + write_pg.species_type = "electron"; + write_pg.x = x; write_pg.y = y; write_pg.z = z; + write_pg.px = px; write_pg.py = py; write_pg.pz = pz; + write_pg.weight = weight; + + /* Write a series with %T expanded to four digits */ + result = pmd_open_series(TEST_TEMP_DIR "/padded_%T.h5", &series, PMD_TRUNC); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + series->iteration_padding = 4; + + for (int64_t index = 1; index <= 2; index++) { + result = pmd_open_iteration(series, index, &iter); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + result = pmd_write_particle_group(iter, &write_pg); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + pmd_close_iteration(iter); + } + pmd_close_series(series); + + /* The padded name is the one that exists on disk */ + TEST_ASSERT_TRUE(path_exists(TEST_TEMP_DIR "/padded_0001.h5")); + TEST_ASSERT_FALSE(path_exists(TEST_TEMP_DIR "/padded_1.h5")); + + /* Reading it back detects the padding and finds both iterations */ + result = pmd_open_series(TEST_TEMP_DIR "/padded_%T.h5", &series, PMD_RDONLY); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_UINT(4, series->iteration_padding); + + result = pmd_list_iterations(series, &iterations, &num_iterations); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_EQUAL_INT(2, num_iterations); + TEST_ASSERT_EQUAL_INT64(1, iterations[0]); + TEST_ASSERT_EQUAL_INT64(2, iterations[1]); + + free(iterations); + pmd_close_series(series); +} + int main(void) { /* Suppress HDF5 error messages during tests */ pmd_set_log_level(PMD_LOG_NONE); @@ -263,6 +332,7 @@ int main(void) { RUN_TEST(test_write_and_read_particle_group_based); RUN_TEST(test_write_and_read_particle_group_file_based); + RUN_TEST(test_write_and_read_zero_padded_file_based); /* Clean up HDF5 library internal resources */ H5close(); diff --git a/tests/test_utilities.c b/tests/test_utilities.c index ad35d24..57e1f3a 100644 --- a/tests/test_utilities.c +++ b/tests/test_utilities.c @@ -92,70 +92,127 @@ void test_extract_iteration_from_name(void) { pmd_status status; /* Simple extraction */ - status = extract_iteration_from_name("data_0", "data_%T", &iteration); + status = extract_iteration_from_name("data_0", "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(0, iteration); - status = extract_iteration_from_name("data_123", "data_%T", &iteration); + status = extract_iteration_from_name("data_123", "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(123, iteration); - status = extract_iteration_from_name("data_999999", "data_%T", &iteration); + status = extract_iteration_from_name("data_999999", "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(999999, iteration); /* With file extension */ - status = extract_iteration_from_name("sim_42.h5", "sim_%T.h5", &iteration); + status = extract_iteration_from_name("sim_42.h5", "sim_%T.h5", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(42, iteration); /* Multiple %T in pattern - should extract the number */ - status = extract_iteration_from_name("data_5_step_5", "data_%T_step_%T", &iteration); + status = extract_iteration_from_name("data_5_step_5", "data_%T_step_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(5, iteration); /* Just %T pattern */ - status = extract_iteration_from_name("123", "%T", &iteration); + status = extract_iteration_from_name("123", "%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(123, iteration); /* Leading zeros */ - status = extract_iteration_from_name("data_00042", "data_%T", &iteration); + status = extract_iteration_from_name("data_00042", "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); TEST_ASSERT_EQUAL_INT64(42, iteration); /* Mismatch cases - should fail */ - status = extract_iteration_from_name("data_abc", "data_%T", &iteration); + status = extract_iteration_from_name("data_abc", "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); - status = extract_iteration_from_name("wrong_5", "data_%T", &iteration); + status = extract_iteration_from_name("wrong_5", "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); - status = extract_iteration_from_name("data_5.txt", "data_%T.h5", &iteration); + status = extract_iteration_from_name("data_5.txt", "data_%T.h5", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); /* Multiple %T with different numbers - should fail */ - status = extract_iteration_from_name("data_5_step_6", "data_%T_step_%T", &iteration); + status = extract_iteration_from_name("data_5_step_6", "data_%T_step_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); + /* Multiple %T padded to different widths - should report a format error */ + status = extract_iteration_from_name("data_0005_step_5", "data_%T_step_%T", &iteration, NULL); + TEST_ASSERT_EQUAL_INT(PMD_ERROR_FILE_FORMAT, status); + /* Consecutive %T patterns - ambiguous, should fail */ - status = extract_iteration_from_name("data_123", "data_%T%T", &iteration); + status = extract_iteration_from_name("data_123", "data_%T%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); - status = extract_iteration_from_name("123", "%T%T", &iteration); + status = extract_iteration_from_name("123", "%T%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); - status = extract_iteration_from_name("sim_456_end", "sim_%T%T_end", &iteration); + status = extract_iteration_from_name("sim_456_end", "sim_%T%T_end", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR, status); /* NULL input */ - status = extract_iteration_from_name(NULL, "data_%T", &iteration); + status = extract_iteration_from_name(NULL, "data_%T", &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR_NULL_POINTER, status); - status = extract_iteration_from_name("data_5", NULL, &iteration); + status = extract_iteration_from_name("data_5", NULL, &iteration, NULL); TEST_ASSERT_EQUAL_INT(PMD_ERROR_NULL_POINTER, status); } +/* ========================================================================= + * extract_iteration_from_name() padding detection tests + * ========================================================================= */ + +void test_extract_iteration_padding(void) { + int64_t iteration; + unsigned int padding; + pmd_status status; + + /* Zero padded names report the full digit width */ + status = extract_iteration_from_name("00001", "%T", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_INT64(1, iteration); + TEST_ASSERT_EQUAL_UINT(5, padding); + + status = extract_iteration_from_name("data_0042.h5", "data_%T.h5", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_INT64(42, iteration); + TEST_ASSERT_EQUAL_UINT(4, padding); + + /* All zeros is still padding when wider than the bare value */ + status = extract_iteration_from_name("0000", "%T", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_INT64(0, iteration); + TEST_ASSERT_EQUAL_UINT(4, padding); + + /* Unpadded names report no padding, including multi digit ones */ + status = extract_iteration_from_name("1", "%T", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_UINT(0, padding); + + status = extract_iteration_from_name("10", "%T", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_INT64(10, iteration); + TEST_ASSERT_EQUAL_UINT(0, padding); + + status = extract_iteration_from_name("0", "%T", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_INT64(0, iteration); + TEST_ASSERT_EQUAL_UINT(0, padding); + + /* Padding is reset on every call, so a stale value cannot leak through */ + padding = 7; + status = extract_iteration_from_name("123", "%T", &iteration, &padding); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_UINT(0, padding); + + /* Padding out is optional */ + status = extract_iteration_from_name("00001", "%T", &iteration, NULL); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, status); + TEST_ASSERT_EQUAL_INT64(1, iteration); +} + /* ========================================================================= * replace_iteration() tests * ========================================================================= */ @@ -164,62 +221,117 @@ void test_replace_iteration(void) { char *result; /* Single %T replacement */ - result = replace_iteration("data_%T.h5", 0); + result = replace_iteration("data_%T.h5", 0, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("data_0.h5", result); free(result); - result = replace_iteration("data_%T.h5", 123); + result = replace_iteration("data_%T.h5", 123, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("data_123.h5", result); free(result); - result = replace_iteration("iter_%T", 42); + result = replace_iteration("iter_%T", 42, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("iter_42", result); free(result); /* Multiple %T replacements - all should get same number */ - result = replace_iteration("data_%T/step_%T/final.h5", 5); + result = replace_iteration("data_%T/step_%T/final.h5", 5, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("data_5/step_5/final.h5", result); free(result); - result = replace_iteration("/path/%T/data_%T.h5", 100); + result = replace_iteration("/path/%T/data_%T.h5", 100, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("/path/100/data_100.h5", result); free(result); - result = replace_iteration("sim_%T_iter_%T_final_%T.h5", 7); + result = replace_iteration("sim_%T_iter_%T_final_%T.h5", 7, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("sim_7_iter_7_final_7.h5", result); free(result); /* No %T in template - should return copy of template */ - result = replace_iteration("no_iteration.h5", 999); + result = replace_iteration("no_iteration.h5", 999, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("no_iteration.h5", result); free(result); /* Just %T */ - result = replace_iteration("%T", 0); + result = replace_iteration("%T", 0, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("0", result); free(result); - result = replace_iteration("%T", 12345); + result = replace_iteration("%T", 12345, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("12345", result); free(result); /* Large iteration numbers */ - result = replace_iteration("data_%T.h5", 1000000); + result = replace_iteration("data_%T.h5", 1000000, 0); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_EQUAL_STRING("data_1000000.h5", result); free(result); /* NULL input */ - result = replace_iteration(NULL, 5); + result = replace_iteration(NULL, 5, 0); + TEST_ASSERT_NULL(result); +} + +/* ========================================================================= + * replace_iteration() padding tests + * ========================================================================= */ + +void test_replace_iteration_padding(void) { + char *result; + + /* Zero padded to the requested width */ + result = replace_iteration("/data/%T/", 1, 5); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("/data/00001/", result); + free(result); + + result = replace_iteration("data_%T.h5", 42, 4); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("data_0042.h5", result); + free(result); + + result = replace_iteration("%T", 0, 3); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("000", result); + free(result); + + /* Every %T gets the same padded width */ + result = replace_iteration("data_%T/step_%T.h5", 7, 3); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("data_007/step_007.h5", result); + free(result); + + /* Padding narrower than the number does not truncate it */ + result = replace_iteration("data_%T.h5", 123456, 3); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("data_123456.h5", result); + free(result); + + /* Padding equal to the number width changes nothing */ + result = replace_iteration("data_%T.h5", 123, 3); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("data_123.h5", result); + free(result); + + /* Padding of 0 is the unpadded behaviour */ + result = replace_iteration("data_%T.h5", 5, 0); + TEST_ASSERT_NOT_NULL(result); + TEST_ASSERT_EQUAL_STRING("data_5.h5", result); + free(result); + + /* Padding wider than the internal buffer is rejected */ + result = replace_iteration("data_%T.h5", 5, 64); + TEST_ASSERT_NULL(result); + + result = replace_iteration("data_%T.h5", 5, 1000); TEST_ASSERT_NULL(result); } @@ -231,7 +343,9 @@ int main(void) { UNITY_BEGIN(); RUN_TEST(test_parse_iteration_pattern); RUN_TEST(test_extract_iteration_from_name); + RUN_TEST(test_extract_iteration_padding); RUN_TEST(test_replace_iteration); + RUN_TEST(test_replace_iteration_padding); /* Clean up HDF5 library internal resources */ H5close(); diff --git a/tests/test_write.c b/tests/test_write.c index e760aa0..50d29d6 100644 --- a/tests/test_write.c +++ b/tests/test_write.c @@ -1300,7 +1300,7 @@ void test_openpmd_required_attributes(void) { file_id = series->file_id; } else { /* For file-based, open first file to check root attributes */ - char *filename = replace_iteration(cases[case_idx].pattern, test_iterations[0]); + char *filename = replace_iteration(cases[case_idx].pattern, test_iterations[0], 0); file_id = H5Fopen(filename, H5F_ACC_RDONLY, H5P_DEFAULT); free(filename); } @@ -2623,7 +2623,7 @@ void test_windows_iteration_format_normalized(void) { TEST_ASSERT_EQUAL_INT_MESSAGE(PMD_SUCCESS, result, cases[i].description); /* Open the created file directly with HDF5 and read iterationFormat attribute */ - char *actual_file = replace_iteration(cases[i].pattern, 5); + char *actual_file = replace_iteration(cases[i].pattern, 5, 0); TEST_ASSERT_NOT_NULL_MESSAGE(actual_file, cases[i].description); /* Normalize path for opening (Windows allows both) */