diff options
| -rw-r--r-- | .circleci/config.yml | 34 | ||||
| -rw-r--r-- | CMakeLists.txt | 26 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | splitfits.c | 63 | ||||
| -rwxr-xr-x | test.sh | 181 | ||||
| -rw-r--r-- | test_config.sh | 11 | ||||
| -rw-r--r-- | tests/test_split.sh | 35 | ||||
| -rw-r--r-- | tests/test_usage.sh | 9 | ||||
| -rw-r--r-- | version.h.in | 6 | 
9 files changed, 361 insertions, 8 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..e412a11 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,34 @@ +version: 2 +jobs: +  build: +    machine: +      image: ubuntu-1604:202004-01 + +    steps: +      - checkout +      - run: | +          set -xe +          mkdir build && cd build +          cmake .. +          make + +  test: +    machine: +      image: ubuntu-1604:202004-01 + +    steps: +      - checkout +      - run: | +          set -xe +          mkdir build && cd build +          cmake .. +          make +          make check + +workflows: +  version: 2 + +  build-and-test: +    jobs: +      - build +      - test diff --git a/CMakeLists.txt b/CMakeLists.txt index ea12358..43f002c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,29 @@ -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.1...3.16)  project(splitfits C) -  set(CMAKE_C_STANDARD 99) +find_package(Git) +if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") +	execute_process(COMMAND ${GIT_EXECUTABLE} describe --always --long --tags --dirty +		OUTPUT_VARIABLE GIT_VERSION +		OUTPUT_STRIP_TRAILING_WHITESPACE) +else() +	message(WARNING "git not found or directory is not a repository") +	set(GIT_VERSION, "unknown") +endif() +  add_executable(splitfits splitfits.c) +include_directories("${CMAKE_CURRENT_BINARY_DIR}") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.h.in" +	"${CMAKE_CURRENT_BINARY_DIR}/version.h" @ONLY) + +file(GLOB TEST_RUNNER "test*.sh") +file(COPY ${TEST_RUNNER} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +file(GLOB TEST_FRAMEWORK "tests") +file(COPY ${TEST_FRAMEWORK} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + + +add_custom_target(check "${CMAKE_CURRENT_BINARY_DIR}/test.sh")  install(TARGETS splitfits -        RUNTIME) +	RUNTIME DESTINATION bin) @@ -13,15 +13,19 @@ CFITSIO is a full-featured library and has quite a few system dependencies. `spl  $ mkdir build  $ cd build  $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local +$ make +$ make check  $ make install  ```  ## Without cmake  ```sh  $ cc -o splitfits splitfits.c +$ ./test.sh  $ install -m 755 splitfits /usr/local/bin  ``` +  # Usage  ``` diff --git a/splitfits.c b/splitfits.c index 7493ed9..6347c4a 100644 --- a/splitfits.c +++ b/splitfits.c @@ -4,12 +4,43 @@  #include <limits.h>  #include <unistd.h>  #include <errno.h> +#include <sys/stat.h> +#include "version.h"  #define OP_SPLIT 0  #define OP_COMBINE 1  #define FITS_BLOCK 2880  #define FITS_RECORD 80 +static int mkdirs(const char *_path, mode_t mode) { +    const char *delim; +    char *parts; +    char *token; +    char path[PATH_MAX]; +    int status; + +    delim = "/"; +    parts = strdup(_path); +    if (parts == NULL) { +        perror(parts); +        exit(1); +    } + +    memset(path, '\0', PATH_MAX); + +    status = -1; +    token = NULL; +    while ((token = strsep(&parts, delim)) != NULL) { +        strcat(path, token); +        strcat(path, delim); +        status = mkdir(path, mode); +        if (status < 0) { +            break; +        } +    } +    return status; +} +  /**   * Check for FITS header keyword in data block   * @param block of memory of size FITS_BLOCK @@ -151,6 +182,8 @@ int split_file(const char *_filename, const char *dest) {      FILE *fp_out;      FILE *map_out;      char outfile[PATH_MAX]; +    char path[PATH_MAX]; +    char filename[PATH_MAX];      char _mapfile[PATH_MAX];      char *mapfile;      char *block; @@ -176,7 +209,8 @@ int split_file(const char *_filename, const char *dest) {      }      mapfile = _mapfile; -    sprintf(mapfile, "%s/%s.part_map", dest ? dest : ".", _filename); +    strcpy(filename, _filename); +    sprintf(mapfile, "%s/%s.part_map", dest ? dest : ".", get_basename(filename));      block_size = FITS_BLOCK;      dataFrame = dataframe_init(); @@ -241,8 +275,6 @@ int split_file(const char *_filename, const char *dest) {      // Read offset from the input files and write it to its respective .part_N file      for (off = 0; off < dataFrame->num_inuse; off++) { -        char path[PATH_MAX]; -        char filename[PATH_MAX];          char *ext;          // Get dirname of input path @@ -263,7 +295,7 @@ int split_file(const char *_filename, const char *dest) {          }          // Create output file name -        sprintf(outfile, "%s/%s", path, filename); +        sprintf(outfile, "%s/%s", path, get_basename(filename));          // Strip file extension from output file          if ((ext = strrchr(outfile, '.')) == NULL) { @@ -455,6 +487,7 @@ void usage(char *program_name) {      printf("   -h  --help       This message\n");      printf("   -c  --combine    Reconstruct original file using .part_map data\n");      printf("   -o  --outdir     Path where output files are stored\n"); +    printf("   -V  --version    Display version\n");  }  int main(int argc, char *argv[]) { @@ -485,19 +518,39 @@ int main(int argc, char *argv[]) {              exit(0);          } else if (strcmp(argv[inputs], "-o") == 0 || strcmp(argv[inputs], "--outdir") == 0) {              inputs++; +            if (argv[inputs] == NULL) { +                fprintf(stderr, "-o requires an argument\n"); +                usage(prog); +                exit(1); +            }              if (access(argv[inputs], R_OK | W_OK | X_OK) != 0) { -                fprintf(stderr, "%s: output directory does not exist or is not writable\n", argv[inputs]); +                printf("Creating output directory: %s\n", argv[inputs]); +                if (mkdirs(argv[inputs], 0755) < 0) { +                    perror(outdir); +                    exit(1); +                }              }              outdir = strdup(argv[inputs]);          } else if (strcmp(argv[inputs], "-c") == 0 || strcmp(argv[inputs], "--combine") == 0) {              // User wants to reconstruct a FITS file using a .part_map              op_mode = OP_COMBINE; +        } else if (strcmp(argv[inputs], "-V") == 0 || strcmp(argv[inputs], "--version") == 0) { +            // Dump version and exit +            puts(SPLITFITS_VERSION); +            exit(0);          } else {              // Arguments beyond this point are considered input file paths              break;          }      } +    // Make sure we have at least one file to process +    if (argc - inputs == 0) { +        fprintf(stderr, "At least one input FILE is required\n"); +        usage(prog); +        exit(1); +    } +      // Make sure all input files exist      bad_files = 0;      for (size_t i = inputs; i < argc; i++) { @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +set -o pipefail + +RTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +source "${RTDIR}"/test_config.sh + +get_data_exists() { +    local url +    local response + +    url="${test_data_remote}/$1" +    if [[ -z $url ]]; then +        echo "get_data_exists: requires a path relative to $test_data_remote" +        exit 1 +    fi + +    response=$(curl -s --head "$url" | head -n 1 | awk '{print $2}') +    if (( $response != 200 )); then +        return 1; +    fi +    return 0; +} + +get_data() { +    local url +    local filename + +    if [[ ! -d "${test_data}" ]]; then +        mkdir -p "${test_data}" +    fi + +    url="${test_data_remote}/$1" +    if [[ -z $url ]]; then +        echo "get_data: requires a path relative to $test_data_remote" +        exit 1 +    fi + +    filename=$(basename $url) +    if ! get_data_exists "$1"; then +        echo "${url}: not found on remote server" >&2 +        return 1 +    fi + +    if ! (cd $test_data && curl -L -O "$url"); then +        echo "Could not download data" >&2 +        return 1 +    fi + +    echo "$test_data/$filename"; +} + +put_data() { +    local url +    local filename + +    url="${test_data_remote}/$2" +    filename="$1" + +    if [[ -z $url ]]; then +        echo "put_data: requires a path relative to $test_data_remote" +        return 1 +    fi + +    if ! curl -s --user "${test_data_remote_auth}" --upload-file "${filename}" "${url}/${filename}"; then +        echo "Could not upload '${filename}' to '${url}'" >&2 +        return 1 +    fi + +    return 0 +} + +put_result() { +    local src; +    local dest; +    src="$1" +    dest="$2" + +    if [[ ! -d "$src" ]]; then +        echo "push_result: source directory does not exist: ${src}" >&2 +        return 1 +    fi + +    if [[ -z "$dest" ]]; then +        echo "push_result: requires a destination relative to ${test_data_remote}" >&2 +        return 1 +    fi + +    for f in $(find "${src}" -type f); do +        echo "Uploading '$f' to '$dest'" +        if ! put_data "$f" "${dest}"; then +            echo "Failed uploading '$src' to '$dest'" >&2 +            return 1 +        fi +    done; +    return 0 +} + +read_tests() { +    for f in "${RTDIR}"/tests/test_*; do +        source "$f" || return 0 +    done +    return 1 +} + +export_tests() { +    tests=( +        $(declare -f | cut -d ' ' -f 1 | grep -E '^test_') +    ) +    total_passed=0 +    total_failed=0 +    total_tests=${#tests[@]} +} + +run_tests() { +    for (( i=0; i < ${total_tests}; i++ )); do +        just_failed=0 +        test_case="${tests[$i]}" +        rm -rf "${test_case}" +        mkdir -p "${test_case}" +        pushd "${test_case}" &>/dev/null +            log_file="output_${test_case}.log" + +            /bin/echo -n -e "[$i] ${test_case}... " +            "$test_case" 2>&1 | tee ${log_file} &>/dev/null +            retval=$? + +            if [[ "$retval" -ne "0" ]]; then +                echo "FAILED ($retval)" +                (( just_failed++ )) +                (( total_failed++ )) +            else +                echo "PASSED" +                (( total_passed++ )) +            fi + +            if [[ ${just_failed} -ne 0 ]] && [[ -s ${log_file} ]]; then +                echo "# OUTPUT:" +                cat "${log_file}" +            fi +        popd &>/dev/null +    done + +    # Upload all artifacts in all test directories +    if [[ -n "$test_data_remote_auth" ]]; then +        echo +        for (( i=0; i < ${total_tests}; i++ )); do +            test_case="${tests[i]}" +            put_result ${test_case} ${test_data_upload} +        done +    fi +} + +check_runtime() { +    if [[ ! -f "${test_program}" ]]; then +        echo "Tests cannot run without: ${test_program}" >&2 +        exit 1 +    fi +} + +show_stats() { +    printf "\n%d test(s): %d passed, %d failed\n" ${total_tests} ${total_passed} ${total_failed} +} + +# main program +check_runtime + +if read_tests; then +    echo "Failed to aggregate tests." >&2 +    exit 1 +fi + +export_tests +run_tests +show_stats + +if (( total_failed )); then +    exit 1 +fi + +exit 0 diff --git a/test_config.sh b/test_config.sh new file mode 100644 index 0000000..7746a6d --- /dev/null +++ b/test_config.sh @@ -0,0 +1,11 @@ +test_program="${RTDIR}/splitfits" +test_program_version="$(git describe --always --tags --long)" +test_data="${RTDIR}/data" +test_data_remote=https://nx.astroconda.org/repository +test_data_remote_auth=${test_data_remote_auth:-} +test_data_upload="generic/spb-splitfits" +if [[ $CIRCLECI == "true" ]]; then +    test_data_upload="${test_data_upload}/ci/${CIRCLE_BRANCH}_${CIRCLE_JOB}/${CIRCLE_BUILD_NUM}/${test_program_version}" +else +    test_data_upload="${test_data_upload}/user/${test_program_version}" +fi diff --git a/tests/test_split.sh b/tests/test_split.sh new file mode 100644 index 0000000..141e320 --- /dev/null +++ b/tests/test_split.sh @@ -0,0 +1,35 @@ +test_splitfits_data1() { +    local datafile +    local retval + +    retval=0 +    datafile=$(get_data generic/fits/sample.fits) +    [[ ! -f ${datafile} ]] && return 1 + +    if ! ${test_program} -o ${test_case} ${datafile}; then +        return 1 +    fi + +    set +x +    return $retval +} + +test_splitfits_combine_data1() { +    local datafile +    local retval + +    retval=0 +    datafile=$(get_data generic/fits/sample.fits) +    [[ ! -f ${datafile} ]] && return 1 + +    if ! ${test_program} -o ${test_case} ${datafile}; then +        return 1 +    fi + +    if ! ${test_program} -c ${test_case}/$(basename ${datafile}).part_map; then +        return 1 +    fi + +    set +x +    return $retval +} diff --git a/tests/test_usage.sh b/tests/test_usage.sh new file mode 100644 index 0000000..2a257d9 --- /dev/null +++ b/tests/test_usage.sh @@ -0,0 +1,9 @@ +test_splitfits_usage_longopt() { +    ${test_program} --help +    return $? +} + +test_splitfits_usage_shortopt() { +    ${test_program} -h +    return $? +} diff --git a/version.h.in b/version.h.in new file mode 100644 index 0000000..581156e --- /dev/null +++ b/version.h.in @@ -0,0 +1,6 @@ +#ifndef SPLITFITS_VERSION_H +#define SPLITFITS_VERSION_H + +#define SPLITFITS_VERSION "@GIT_VERSION@" + +#endif  | 
