From 18295c700c4c98d741f92f5260968770426e86a6 Mon Sep 17 00:00:00 2001 From: Tiago Quintino Date: Wed, 11 Mar 2026 08:22:59 +0000 Subject: [PATCH 1/6] Add more tests courtesy of Claude --- tests/CMakeLists.txt | 15 +- tests/grib_corrupted_messages.sh | 110 ++++++++++ tests/grib_packing_roundtrip.sh | 95 +++++++++ tests/test_bits.cc | 344 ++++++++++++++++++++++++++++++ tests/test_data_structures.cc | 230 ++++++++++++++++++++ tests/test_expression.cc | 232 +++++++++++++++++++++ tests/test_float_conversions.cc | 272 ++++++++++++++++++++++++ tests/test_geo.cc | 234 +++++++++++++++++++++ tests/test_io.cc | 304 +++++++++++++++++++++++++++ tests/test_unit_extended.sh | 43 ++++ tests/test_value_api.cc | 348 +++++++++++++++++++++++++++++++ 11 files changed, 2225 insertions(+), 2 deletions(-) create mode 100755 tests/grib_corrupted_messages.sh create mode 100755 tests/grib_packing_roundtrip.sh create mode 100644 tests/test_bits.cc create mode 100644 tests/test_data_structures.cc create mode 100644 tests/test_expression.cc create mode 100644 tests/test_float_conversions.cc create mode 100644 tests/test_geo.cc create mode 100644 tests/test_io.cc create mode 100755 tests/test_unit_extended.sh create mode 100644 tests/test_value_api.cc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 47a2b2a01..3b5f8fad8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -75,7 +75,14 @@ list(APPEND test_c_bins grib_sh_imag grib_spectral grib_lam_bf - grib_lam_gp) + grib_lam_gp + test_bits + test_float_conversions + test_value_api + test_expression + test_data_structures + test_geo + test_io) foreach( tool ${test_c_bins} ) @@ -184,6 +191,8 @@ if( HAVE_BUILD_TOOLS ) grib_stattype grib_ecc-2221 grib_ecc-2218 + test_unit_extended + grib_corrupted_messages ) # These tests require data downloads @@ -420,7 +429,9 @@ if( HAVE_BUILD_TOOLS ) grib_neg_fctime codes_split_file grib_mars_keys1 - grib_mars_keys2) + grib_mars_keys2 + grib_packing_roundtrip + ) if( HAVE_AEC AND ENABLE_EXTRA_TESTS ) list(APPEND tests_extra grib_ecc-1431) diff --git a/tests/grib_corrupted_messages.sh b/tests/grib_corrupted_messages.sh new file mode 100755 index 000000000..51e83b442 --- /dev/null +++ b/tests/grib_corrupted_messages.sh @@ -0,0 +1,110 @@ +#!/bin/sh +# (C) Copyright 2005- ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# +# In applying this licence, ECMWF does not waive the privileges and immunities granted to it by +# virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. +# + +. ./include.ctest.sh + +label="corrupted_messages" + +# ----------------------------------------------- +# Test that corrupted/truncated GRIB/BUFR messages +# are handled gracefully (no crashes, proper error codes) +# ----------------------------------------------- + +tempGrib=temp.$label.grib +tempBufr=temp.$label.bufr +tempCorrupt=temp.$label.corrupt +tempOut=temp.$label.out + +# --- Test 1: Truncated GRIB message --- +echo "Test: Truncated GRIB message..." +${tools_dir}/grib_set -s edition=2 $ECCODES_SAMPLES_PATH/GRIB2.tmpl $tempGrib +size=$(wc -c < $tempGrib | tr -d ' ') +half=$((size / 2)) + +# Truncate to first half +dd if=$tempGrib of=$tempCorrupt bs=1 count=$half 2>/dev/null +set +e +${tools_dir}/grib_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +# Should fail (non-zero exit), not crash +echo "Truncated GRIB: grib_ls exit code=$status" + +# --- Test 2: Truncated BUFR message --- +echo "Test: Truncated BUFR message..." +# Copy BUFR sample directly (grib_set cannot handle BUFR files) +cp $ECCODES_SAMPLES_PATH/BUFR4.tmpl $tempBufr +size=$(wc -c < $tempBufr | tr -d ' ') +half=$((size / 2)) + +dd if=$tempBufr of=$tempCorrupt bs=1 count=$half 2>/dev/null +set +e +${tools_dir}/bufr_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +echo "Truncated BUFR: bufr_ls exit code=$status" + +# --- Test 3: Corrupted magic number --- +echo "Test: Corrupted magic number..." +cp $tempGrib $tempCorrupt +# Replace first byte 'G' (0x47) with 'X' (0x58) +printf '\x58' | dd of=$tempCorrupt bs=1 count=1 conv=notrunc 2>/dev/null +set +e +${tools_dir}/grib_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +echo "Corrupted magic: grib_ls exit code=$status" +# Should fail or report no messages, not crash + +# --- Test 4: File with just "GRIB" header and nothing else --- +echo "Test: GRIB header only (4 bytes)..." +printf 'GRIB' > $tempCorrupt +set +e +${tools_dir}/grib_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +echo "GRIB header only: grib_ls exit code=$status" + +# --- Test 5: Empty file --- +echo "Test: Empty file..." +> $tempCorrupt +set +e +${tools_dir}/grib_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +echo "Empty file: grib_ls exit code=$status" + +# --- Test 6: Random bytes file --- +echo "Test: Random bytes..." +dd if=/dev/urandom of=$tempCorrupt bs=1 count=256 2>/dev/null +set +e +${tools_dir}/grib_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +echo "Random bytes: grib_ls exit code=$status" + +# --- Test 7: Missing 7777 end marker --- +echo "Test: Missing 7777 end marker..." +cp $tempGrib $tempCorrupt +size=$(wc -c < $tempCorrupt | tr -d ' ') +truncsize=$((size - 4)) +dd if=$tempGrib of=$tempCorrupt bs=1 count=$truncsize 2>/dev/null +# Append something that is NOT 7777 +printf '\x00\x00\x00\x00' >> $tempCorrupt +set +e +${tools_dir}/grib_ls $tempCorrupt > $tempOut 2>&1 +status=$? +set -e +echo "Bad end marker: grib_ls exit code=$status" + +echo "" +echo "All corrupted message tests completed without crashes." + +rm -f $tempGrib $tempBufr $tempCorrupt $tempOut diff --git a/tests/grib_packing_roundtrip.sh b/tests/grib_packing_roundtrip.sh new file mode 100755 index 000000000..084fd6663 --- /dev/null +++ b/tests/grib_packing_roundtrip.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# (C) Copyright 2005- ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# +# In applying this licence, ECMWF does not waive the privileges and immunities granted to it by +# virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. +# + +. ./include.ctest.sh + +label="packing_roundtrip" + +# ----------------------------------------------- +# Test packing round-trip: encode values, decode, compare +# This ensures different packing types preserve data integrity +# ----------------------------------------------- + +tempGrib=temp.$label.grib +tempOut=temp.$label.out +tempRef=temp.$label.ref +tempFilt=temp.$label.filt + +# Create a GRIB2 message from sample with known grid dimensions +${tools_dir}/grib_set -s Ni=36,Nj=18,numberOfDataPoints=648,numberOfValues=648 \ + $ECCODES_SAMPLES_PATH/GRIB2.tmpl $tempGrib + +# Generate ascending test values (temperature-like) +cat > $tempFilt << EOF +set bitsPerValue = 16; +set Ni = 36; +set Nj = 18; +set numberOfDataPoints = 648; +set numberOfValues = 648; +# Set values to a ramp from 200 to 320 (temperature range in K) +set values = {$(seq 200 0.1855 320 | head -648 | tr '\n' ',' | sed 's/,$//')}; +write; +EOF + +# Test simple packing (grid_simple) - the default +${tools_dir}/grib_filter -o $tempGrib $tempFilt $ECCODES_SAMPLES_PATH/GRIB2.tmpl +grib_check_key_equals $tempGrib packingType grid_simple + +stats=$(${tools_dir}/grib_get -p max,min,avg $tempGrib) +echo "Simple packing stats: $stats" + +# Verify we can read back the values +${tools_dir}/grib_get_data $tempGrib > $tempOut +count=$(wc -l < $tempOut | tr -d ' ') +# account for header line +test "$count" -gt 0 + +# Test grid_second_order packing if data file is big enough +if [ "$HAVE_AEC" = "1" ]; then + tempCcsds=temp.${label}.ccsds.grib + ${tools_dir}/grib_set -s packingType=grid_ccsds $tempGrib $tempCcsds + grib_check_key_equals $tempCcsds packingType grid_ccsds + + # Compare original and CCSDS-packed values + val1=$(${tools_dir}/grib_get -p avg $tempGrib) + val2=$(${tools_dir}/grib_get -p avg $tempCcsds) + # They should be close (within packing tolerance) + echo "Simple avg=$val1, CCSDS avg=$val2" + rm -f $tempCcsds +fi + +# Test changing bitsPerValue +for bpv in 8 12 16 24; do + tempBpv=temp.${label}.bpv${bpv}.grib + ${tools_dir}/grib_set -s bitsPerValue=$bpv $tempGrib $tempBpv + bpv_out=$(${tools_dir}/grib_get -p bitsPerValue $tempBpv) + test "$bpv_out" = "$bpv" + rm -f $tempBpv +done + +# Test constant field (all values the same) +cat > $tempFilt << EOF +set bitsPerValue = 16; +set Ni = 10; +set Nj = 10; +set numberOfDataPoints = 100; +set numberOfValues = 100; +set values = {$(python3 -c "print(','.join(['273.15']*100))")}; +write; +EOF +tempConst=temp.${label}.const.grib +${tools_dir}/grib_filter -o $tempConst $tempFilt $ECCODES_SAMPLES_PATH/GRIB2.tmpl 2>/dev/null || true +if [ -f "$tempConst" ]; then + max=$(${tools_dir}/grib_get -p max $tempConst) + min=$(${tools_dir}/grib_get -p min $tempConst) + echo "Constant field: max=$max min=$min" +fi + +rm -f $tempGrib $tempOut $tempRef $tempFilt $tempConst diff --git a/tests/test_bits.cc b/tests/test_bits.cc new file mode 100644 index 000000000..5e95ea2ea --- /dev/null +++ b/tests/test_bits.cc @@ -0,0 +1,344 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for bit manipulation functions (grib_bits.cc / grib_bits_any_endian.cc) + * + * Tests encode/decode round-trips at various bit widths, boundary values, + * signed encoding, string encoding, and the grib_is_all_bits_one function. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: unsigned long encode/decode round-trip ========== */ +static void test_unsigned_long_roundtrip() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[16]; + + /* Test various bit widths from 1 to 32 */ + for (int nbits = 1; nbits <= 32; nbits++) { + unsigned long maxVal = (1UL << nbits) - 1; + unsigned long testVals[] = {0, 1, maxVal / 2, maxVal - 1, maxVal}; + + for (int t = 0; t < 5; t++) { + unsigned long val = testVals[t]; + if (val > maxVal) continue; + + memset(buf, 0, sizeof(buf)); + long bitp_enc = 0; + int err = grib_encode_unsigned_long(buf, val, &bitp_enc, nbits); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(bitp_enc == nbits); + + long bitp_dec = 0; + unsigned long decoded = grib_decode_unsigned_long(buf, &bitp_dec, nbits); + TEST_ASSERT(bitp_dec == nbits); + if (decoded != val) { + fprintf(stderr, " MISMATCH nbits=%d val=%lu decoded=%lu\n", nbits, val, decoded); + } + TEST_ASSERT(decoded == val); + } + } + TEST_PASS(); +} + +/* ========== Test: unaligned bit offsets ========== */ +static void test_unaligned_offsets() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[32]; + + /* Write multiple values at non-byte-aligned positions */ + memset(buf, 0, sizeof(buf)); + long bitp = 0; + + /* Write 3-bit value = 5 (101) */ + grib_encode_unsigned_long(buf, 5, &bitp, 3); + TEST_ASSERT(bitp == 3); + + /* Write 5-bit value = 17 (10001) */ + grib_encode_unsigned_long(buf, 17, &bitp, 5); + TEST_ASSERT(bitp == 8); + + /* Write 13-bit value = 4096 */ + grib_encode_unsigned_long(buf, 4096, &bitp, 13); + TEST_ASSERT(bitp == 21); + + /* Write 11-bit value = 1023 */ + grib_encode_unsigned_long(buf, 1023, &bitp, 11); + TEST_ASSERT(bitp == 32); + + /* Now decode them back */ + bitp = 0; + unsigned long v1 = grib_decode_unsigned_long(buf, &bitp, 3); + TEST_ASSERT(v1 == 5); + + unsigned long v2 = grib_decode_unsigned_long(buf, &bitp, 5); + TEST_ASSERT(v2 == 17); + + unsigned long v3 = grib_decode_unsigned_long(buf, &bitp, 13); + TEST_ASSERT(v3 == 4096); + + unsigned long v4 = grib_decode_unsigned_long(buf, &bitp, 11); + TEST_ASSERT(v4 == 1023); + + TEST_PASS(); +} + +/* ========== Test: signed long encode/decode ========== */ +static void test_signed_long_roundtrip() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[16]; + + /* Test byte-level signed encode/decode */ + long test_vals[] = {0, 1, -1, 127, -127, 255, -255, 32767, -32767, 100000, -100000}; + + for (int i = 0; i < 11; i++) { + long val = test_vals[i]; + memset(buf, 0, sizeof(buf)); + + /* Use 4 bytes */ + grib_encode_signed_long(buf, val, 0, 4); + long decoded = grib_decode_signed_long(buf, 0, 4); + TEST_ASSERT(decoded == val); + } + + /* Test bit-level signed encode/decode */ + for (int nbits = 2; nbits <= 32; nbits++) { + long maxPos = (1L << (nbits - 1)) - 1; + long testBitVals[] = {0, 1, -1, maxPos, -maxPos}; + + for (int t = 0; t < 5; t++) { + long val = testBitVals[t]; + memset(buf, 0, sizeof(buf)); + + long bitp_enc = 0; + grib_encode_signed_longb(buf, val, &bitp_enc, nbits); + + long bitp_dec = 0; + long decoded = grib_decode_signed_longb(buf, &bitp_dec, nbits); + TEST_ASSERT(decoded == val); + } + } + + TEST_PASS(); +} + +/* ========== Test: get/set individual bits ========== */ +static void test_get_set_bit() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[4]; + + memset(buf, 0, sizeof(buf)); + + /* Set specific bits and verify */ + long bitp = 0; + grib_set_bit_on(buf, &bitp); /* bit 0 = 1 */ + TEST_ASSERT(bitp == 1); + TEST_ASSERT(grib_get_bit(buf, 0) != 0); + + grib_set_bit_on(buf, &bitp); /* bit 1 = 1 */ + TEST_ASSERT(grib_get_bit(buf, 1) != 0); + + /* bit 2 should still be 0 */ + TEST_ASSERT(grib_get_bit(buf, 2) == 0); + + /* Use grib_set_bit to explicitly set and clear */ + grib_set_bit(buf, 7, 1); /* set bit 7 */ + TEST_ASSERT(grib_get_bit(buf, 7) != 0); + + grib_set_bit(buf, 7, 0); /* clear bit 7 */ + TEST_ASSERT(grib_get_bit(buf, 7) == 0); + + TEST_PASS(); +} + +/* ========== Test: zero bits decode ========== */ +static void test_zero_bits() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + long bitp = 0; + unsigned long result = grib_decode_unsigned_long(buf, &bitp, 0); + TEST_ASSERT(result == 0); + TEST_ASSERT(bitp == 0); /* bitp should not advance */ + + TEST_PASS(); +} + +/* ========== Test: grib_is_all_bits_one ========== */ +static void test_is_all_bits_one() +{ + printf("Running %s ...\n", __func__); + + /* 8 bits all one = 255 = 0xFF */ + TEST_ASSERT(grib_is_all_bits_one(0xFF, 8) == 1); + TEST_ASSERT(grib_is_all_bits_one(0xFE, 8) == 0); + + /* 1 bit all one = 1 */ + TEST_ASSERT(grib_is_all_bits_one(1, 1) == 1); + TEST_ASSERT(grib_is_all_bits_one(0, 1) == 0); + + /* 16 bits all one = 65535 */ + TEST_ASSERT(grib_is_all_bits_one(0xFFFF, 16) == 1); + TEST_ASSERT(grib_is_all_bits_one(0xFFFE, 16) == 0); + + /* 32 bits all one */ + TEST_ASSERT(grib_is_all_bits_one(0xFFFFFFFF, 32) == 1); + + TEST_PASS(); +} + +/* ========== Test: string encode/decode ========== */ +static void test_string_encode_decode() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[64]; + char decoded[32]; + + /* Byte-aligned string encode/decode */ + memset(buf, 0, sizeof(buf)); + memset(decoded, 0, sizeof(decoded)); + + long bitp = 0; + const char* test_str = "GRIB"; + int err = grib_encode_string(buf, &bitp, 4, test_str); + TEST_ASSERT(err == 0); + TEST_ASSERT(bitp == 32); + + bitp = 0; + grib_decode_string(buf, &bitp, 4, decoded); + TEST_ASSERT(memcmp(decoded, "GRIB", 4) == 0); + + /* Non-byte-aligned string encode/decode */ + memset(buf, 0, sizeof(buf)); + bitp = 3; /* Start at bit offset 3 */ + err = grib_encode_string(buf, &bitp, 4, "TEST"); + TEST_ASSERT(err == 0); + TEST_ASSERT(bitp == 35); + + bitp = 3; + memset(decoded, 0, sizeof(decoded)); + grib_decode_string(buf, &bitp, 4, decoded); + TEST_ASSERT(memcmp(decoded, "TEST", 4) == 0); + + TEST_PASS(); +} + +/* ========== Test: unsigned byte long decode ========== */ +static void test_decode_unsigned_byte_long() +{ + printf("Running %s ...\n", __func__); + unsigned char buf[] = {0x01, 0x02, 0x03, 0x04}; + + /* Read 1 byte at offset 0 */ + unsigned long v = grib_decode_unsigned_byte_long(buf, 0, 1); + TEST_ASSERT(v == 0x01); + + /* Read 2 bytes at offset 0 */ + v = grib_decode_unsigned_byte_long(buf, 0, 2); + TEST_ASSERT(v == 0x0102); + + /* Read 4 bytes at offset 0 */ + v = grib_decode_unsigned_byte_long(buf, 0, 4); + TEST_ASSERT(v == 0x01020304); + + /* Read 2 bytes at offset 2 */ + v = grib_decode_unsigned_byte_long(buf, 2, 2); + TEST_ASSERT(v == 0x0304); + + TEST_PASS(); +} + +/* ========== Test: sequential encode/decode of multiple values ========== */ +static void test_sequential_values() +{ + printf("Running %s ...\n", __func__); + + /* Buffer must be large enough for sum of all bit widths. + * widths cycle 1..32, so 100 values: 3 full cycles (1..32) + partial. + * Sum of 1..32 = 528, * 3 = 1584, plus partial = ~1650 bits = ~207 bytes. + * Use 256 bytes to be safe. */ + unsigned char buf[256]; + memset(buf, 0, sizeof(buf)); + + /* Encode a series of values with varying bit widths */ + long bitp_enc = 0; + const int count = 100; + unsigned long values[100]; + int widths[100]; + + srand(42); + for (int i = 0; i < count; i++) { + widths[i] = (i % 16) + 1; /* 1 to 16 bits */ + unsigned long maxVal = (1UL << widths[i]) - 1; + values[i] = (unsigned long)(rand()) % (maxVal + 1); + + grib_encode_unsigned_long(buf, values[i], &bitp_enc, widths[i]); + } + + /* Decode and verify */ + long bitp_dec = 0; + for (int i = 0; i < count; i++) { + unsigned long decoded = grib_decode_unsigned_long(buf, &bitp_dec, widths[i]); + if (decoded != values[i]) { + fprintf(stderr, " MISMATCH at i=%d width=%d val=%lu decoded=%lu\n", + i, widths[i], values[i], decoded); + } + TEST_ASSERT(decoded == values[i]); + } + + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running bit manipulation unit tests...\n\n"); + + test_unsigned_long_roundtrip(); + test_unaligned_offsets(); + test_signed_long_roundtrip(); + test_get_set_bit(); + test_zero_bits(); + test_is_all_bits_one(); + test_string_encode_decode(); + test_decode_unsigned_byte_long(); + test_sequential_values(); + + printf("\n========================================\n"); + printf("Bit tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_data_structures.cc b/tests/test_data_structures.cc new file mode 100644 index 000000000..e0bb4b424 --- /dev/null +++ b/tests/test_data_structures.cc @@ -0,0 +1,230 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for internal data structures: + * - grib_iarray (dynamic integer array) + * - grib_darray (dynamic double array) + * - grib_sarray (dynamic string array) + * - codes_power (scaling function) + */ + +#include "eccodes.h" +#include "grib_api_internal.h" +#include "grib_scaling.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: iarray basic operations ========== */ +static void test_iarray_basic() +{ + printf("Running %s ...\n", __func__); + + grib_iarray* a = grib_iarray_new(4, 4); + TEST_ASSERT(a != NULL); + TEST_ASSERT(grib_iarray_used_size(a) == 0); + + grib_iarray_push(a, 10); + grib_iarray_push(a, 20); + grib_iarray_push(a, 30); + TEST_ASSERT(grib_iarray_used_size(a) == 3); + + long* data = grib_iarray_get_array(a); + TEST_ASSERT(data != NULL); + TEST_ASSERT(data[0] == 10); + TEST_ASSERT(data[1] == 20); + TEST_ASSERT(data[2] == 30); + free(data); + + grib_iarray_delete(a); + TEST_PASS(); +} + +/* ========== Test: iarray growth beyond initial capacity ========== */ +static void test_iarray_growth() +{ + printf("Running %s ...\n", __func__); + + /* Start with capacity of 2, increment of 2 */ + grib_iarray* a = grib_iarray_new(2, 2); + TEST_ASSERT(a != NULL); + + /* Push more values than initial capacity */ + for (int i = 0; i < 100; i++) { + grib_iarray_push(a, i * 10); + } + TEST_ASSERT(grib_iarray_used_size(a) == 100); + + long* data = grib_iarray_get_array(a); + for (int i = 0; i < 100; i++) { + TEST_ASSERT(data[i] == i * 10); + } + free(data); + + grib_iarray_delete(a); + TEST_PASS(); +} + +/* ========== Test: darray basic operations ========== */ +static void test_darray_basic() +{ + printf("Running %s ...\n", __func__); + + grib_darray* a = grib_darray_new(4, 4); + TEST_ASSERT(a != NULL); + TEST_ASSERT(grib_darray_used_size(a) == 0); + + grib_darray_push(a, 1.5); + grib_darray_push(a, -2.7); + grib_darray_push(a, 0.0); + grib_darray_push(a, 999.999); + TEST_ASSERT(grib_darray_used_size(a) == 4); + + /* Access internal array directly (no get_array API) */ + TEST_ASSERT(a->v != NULL); + TEST_ASSERT(fabs(a->v[0] - 1.5) < 1e-10); + TEST_ASSERT(fabs(a->v[1] - (-2.7)) < 1e-10); + TEST_ASSERT(fabs(a->v[2] - 0.0) < 1e-10); + TEST_ASSERT(fabs(a->v[3] - 999.999) < 1e-10); + + grib_darray_delete(a); + TEST_PASS(); +} + +/* ========== Test: darray growth ========== */ +static void test_darray_growth() +{ + printf("Running %s ...\n", __func__); + + grib_darray* a = grib_darray_new(1, 1); + TEST_ASSERT(a != NULL); + + for (int i = 0; i < 500; i++) { + grib_darray_push(a, (double)i * 0.1); + } + TEST_ASSERT(grib_darray_used_size(a) == 500); + + /* Access internal array directly */ + for (int i = 0; i < 500; i++) { + TEST_ASSERT(fabs(a->v[i] - (double)i * 0.1) < 1e-10); + } + + grib_darray_delete(a); + TEST_PASS(); +} + +/* ========== Test: sarray basic operations ========== */ +static void test_sarray_basic() +{ + printf("Running %s ...\n", __func__); + + grib_sarray* a = grib_sarray_new(4, 4); + TEST_ASSERT(a != NULL); + TEST_ASSERT(grib_sarray_used_size(a) == 0); + + char s1[] = "hello"; + char s2[] = "world"; + grib_sarray_push(a, s1); + grib_sarray_push(a, s2); + TEST_ASSERT(grib_sarray_used_size(a) == 2); + + grib_sarray_delete(a); + TEST_PASS(); +} + +/* ========== Test: viarray (vector of iarray) ========== */ +static void test_viarray() +{ + printf("Running %s ...\n", __func__); + + grib_viarray* va = grib_viarray_new(2, 2); + TEST_ASSERT(va != NULL); + + grib_iarray* a1 = grib_iarray_new(4, 4); + grib_iarray_push(a1, 1); + grib_iarray_push(a1, 2); + + grib_iarray* a2 = grib_iarray_new(4, 4); + grib_iarray_push(a2, 10); + grib_iarray_push(a2, 20); + grib_iarray_push(a2, 30); + + grib_viarray_push(va, a1); + grib_viarray_push(va, a2); + + grib_iarray_delete(a1); + grib_iarray_delete(a2); + grib_viarray_delete(va); + TEST_PASS(); +} + +/* ========== Test: codes_power template function ========== */ +static void test_codes_power() +{ + printf("Running %s ...\n", __func__); + + /* 2^0 = 1 */ + TEST_ASSERT(fabs(codes_power(0, 2) - 1.0) < 1e-10); + + /* 2^1 = 2 */ + TEST_ASSERT(fabs(codes_power(1, 2) - 2.0) < 1e-10); + + /* 2^10 = 1024 */ + TEST_ASSERT(fabs(codes_power(10, 2) - 1024.0) < 1e-10); + + /* 10^3 = 1000 */ + TEST_ASSERT(fabs(codes_power(3, 10) - 1000.0) < 1e-10); + + /* 2^-1 = 0.5 */ + TEST_ASSERT(fabs(codes_power(-1, 2) - 0.5) < 1e-10); + + /* 2^-3 = 0.125 */ + TEST_ASSERT(fabs(codes_power(-3, 2) - 0.125) < 1e-10); + + /* 10^-2 = 0.01 */ + TEST_ASSERT(fabs(codes_power(-2, 10) - 0.01) < 1e-10); + + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running data structure unit tests...\n\n"); + + test_iarray_basic(); + test_iarray_growth(); + test_darray_basic(); + test_darray_growth(); + test_sarray_basic(); + test_viarray(); + test_codes_power(); + + printf("\n========================================\n"); + printf("Data structure tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_expression.cc b/tests/test_expression.cc new file mode 100644 index 000000000..e9354e742 --- /dev/null +++ b/tests/test_expression.cc @@ -0,0 +1,232 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for expression evaluation subsystem. + * + * Tests True, Long, Double, IsInteger, SubString, Length, Accessor + * expressions for correct evaluation of evaluate_long and evaluate_double. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: True expression ========== */ +static void test_true_expression() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_handle* h = grib_handle_new_from_samples(c, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_expression* e = new_true_expression(c); + TEST_ASSERT(e != NULL); + + double dval = -1; + int err = e->evaluate_double(h, &dval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(dval == 1.0); + + long lval = -1; + err = e->evaluate_long(h, &lval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(lval == 1); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: Long expression ========== */ +static void test_long_expression() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_handle* h = grib_handle_new_from_samples(c, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_expression* e = new_long_expression(c, 42); + TEST_ASSERT(e != NULL); + + long lval = 0; + int err = e->evaluate_long(h, &lval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(lval == 42); + + double dval = 0; + err = e->evaluate_double(h, &dval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(fabs(dval - 42.0) < 1e-10); + + /* Test with zero */ + grib_expression* e0 = new_long_expression(c, 0); + TEST_ASSERT(e0 != NULL); + err = e0->evaluate_long(h, &lval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(lval == 0); + + /* Test with negative */ + grib_expression* eNeg = new_long_expression(c, -100); + TEST_ASSERT(eNeg != NULL); + err = eNeg->evaluate_long(h, &lval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(lval == -100); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: Double expression ========== */ +static void test_double_expression() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_handle* h = grib_handle_new_from_samples(c, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_expression* e = new_double_expression(c, 3.14159); + TEST_ASSERT(e != NULL); + + double dval = 0; + int err = e->evaluate_double(h, &dval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(fabs(dval - 3.14159) < 1e-10); + + long lval = 0; + err = e->evaluate_long(h, &lval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(lval == 3); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: IsInteger expression ========== */ +static void test_is_integer_expression() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_handle* h = grib_handle_new_from_samples(c, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* "edition" is a long (integer) key */ + grib_expression* e = new_is_integer_expression(c, "edition", 0, 1); + TEST_ASSERT(e != NULL); + + double dval = -1; + int err = e->evaluate_double(h, &dval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(dval > 0); /* edition = 2, which is an integer */ + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: Accessor expression (reads key value) ========== */ +static void test_accessor_expression() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_handle* h = grib_handle_new_from_samples(c, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_expression* e = new_accessor_expression(c, "edition", 0, 0); + TEST_ASSERT(e != NULL); + + long lval = 0; + int err = e->evaluate_long(h, &lval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(lval == 2); + + double dval = 0; + err = e->evaluate_double(h, &dval); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(fabs(dval - 2.0) < 1e-10); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: class_name is set for all expression types ========== */ +static void test_expression_class_names() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + + grib_expression* eTrue = new_true_expression(c); + TEST_ASSERT(eTrue != NULL); + TEST_ASSERT(eTrue->class_name() != NULL); + TEST_ASSERT(strlen(eTrue->class_name()) > 0); + + grib_expression* eLong = new_long_expression(c, 0); + TEST_ASSERT(eLong != NULL); + TEST_ASSERT(eLong->class_name() != NULL); + + grib_expression* eDouble = new_double_expression(c, 0.0); + TEST_ASSERT(eDouble != NULL); + TEST_ASSERT(eDouble->class_name() != NULL); + + grib_expression* eLogAnd = new_logical_and_expression(c, NULL, NULL); + TEST_ASSERT(eLogAnd != NULL); + TEST_ASSERT(eLogAnd->class_name() != NULL); + + grib_expression* eLogOr = new_logical_or_expression(c, NULL, NULL); + TEST_ASSERT(eLogOr != NULL); + TEST_ASSERT(eLogOr->class_name() != NULL); + + grib_expression* eStrCmp = new_string_compare_expression(c, NULL, NULL, NULL); + TEST_ASSERT(eStrCmp != NULL); + TEST_ASSERT(eStrCmp->class_name() != NULL); + + grib_expression* eUnOp = new_unop_expression(c, NULL, NULL, NULL); + TEST_ASSERT(eUnOp != NULL); + TEST_ASSERT(eUnOp->class_name() != NULL); + + grib_expression* eBinOp = new_binop_expression(c, NULL, NULL, NULL, NULL); + TEST_ASSERT(eBinOp != NULL); + TEST_ASSERT(eBinOp->class_name() != NULL); + + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running expression evaluation unit tests...\n\n"); + + test_true_expression(); + test_long_expression(); + test_double_expression(); + test_is_integer_expression(); + test_accessor_expression(); + test_expression_class_names(); + + printf("\n========================================\n"); + printf("Expression tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_float_conversions.cc b/tests/test_float_conversions.cc new file mode 100644 index 000000000..ae540ea9e --- /dev/null +++ b/tests/test_float_conversions.cc @@ -0,0 +1,272 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for IBM and IEEE float conversion functions. + * + * Tests round-trip encode/decode, nearest-smaller-float, edge cases (zero, + * negative, small/large values), and the error function. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== IBM Float Tests ========== */ + +static void test_ibm_roundtrip_zero() +{ + printf("Running %s ...\n", __func__); + unsigned long encoded = grib_ibm_to_long(0.0); + double decoded = grib_long_to_ibm(encoded); + TEST_ASSERT(decoded == 0.0); + TEST_PASS(); +} + +static void test_ibm_roundtrip_positive() +{ + printf("Running %s ...\n", __func__); + double test_values[] = {1.0, 0.5, 100.0, 273.15, 1013.25, 0.001, 99999.0}; + + for (int i = 0; i < 7; i++) { + double val = test_values[i]; + unsigned long encoded = grib_ibm_to_long(val); + double decoded = grib_long_to_ibm(encoded); + double rel_err = fabs(decoded - val) / fabs(val); + TEST_ASSERT(rel_err < 1e-6); + } + TEST_PASS(); +} + +static void test_ibm_roundtrip_negative() +{ + printf("Running %s ...\n", __func__); + double test_values[] = {-1.0, -0.5, -273.15, -50000.0}; + + for (int i = 0; i < 4; i++) { + double val = test_values[i]; + unsigned long encoded = grib_ibm_to_long(val); + double decoded = grib_long_to_ibm(encoded); + + /* Check sign bit (bit 31) is set for negative */ + TEST_ASSERT((encoded & 0x80000000) != 0); + + double rel_err = fabs(decoded - val) / fabs(val); + TEST_ASSERT(rel_err < 1e-6); + } + TEST_PASS(); +} + +static void test_ibm_nearest_smaller() +{ + printf("Running %s ...\n", __func__); + double ret = 0; + int err = 0; + + /* The nearest smaller IBM float of a value should be <= the value */ + double test_values[] = {0.0, 1.0, -1.0, 1.5, 273.15, -50.0, 0.001}; + + for (int i = 0; i < 7; i++) { + err = grib_nearest_smaller_ibm_float(test_values[i], &ret); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(ret <= test_values[i]); + } + + /* Zero should map to exactly zero */ + err = grib_nearest_smaller_ibm_float(0.0, &ret); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(ret == 0.0); + + TEST_PASS(); +} + +static void test_ibm_error_function() +{ + printf("Running %s ...\n", __func__); + /* The error function should return a positive value for positive input */ + double err_val = grib_ibmfloat_error(100.0); + TEST_ASSERT(err_val > 0.0); + + /* Error for larger values should generally be >= error for smaller values */ + double err_small = grib_ibmfloat_error(1.0); + double err_large = grib_ibmfloat_error(1e6); + TEST_ASSERT(err_large >= err_small); + + TEST_PASS(); +} + +/* ========== IEEE Float Tests ========== */ + +static void test_ieee_roundtrip_zero() +{ + printf("Running %s ...\n", __func__); + unsigned long encoded = grib_ieee_to_long(0.0); + double decoded = grib_long_to_ieee(encoded); + TEST_ASSERT(decoded == 0.0); + TEST_PASS(); +} + +static void test_ieee_roundtrip_positive() +{ + printf("Running %s ...\n", __func__); + double test_values[] = {1.0, 0.5, 100.0, 273.15, 1013.25, 0.001, 99999.0}; + + for (int i = 0; i < 7; i++) { + double val = test_values[i]; + unsigned long encoded = grib_ieee_to_long(val); + double decoded = grib_long_to_ieee(encoded); + double rel_err = fabs(decoded - val) / fabs(val); + TEST_ASSERT(rel_err < 1e-6); + } + TEST_PASS(); +} + +static void test_ieee_roundtrip_negative() +{ + printf("Running %s ...\n", __func__); + double test_values[] = {-1.0, -0.5, -273.15, -50000.0}; + + for (int i = 0; i < 4; i++) { + double val = test_values[i]; + unsigned long encoded = grib_ieee_to_long(val); + double decoded = grib_long_to_ieee(encoded); + + /* Check sign bit (bit 31) is set for negative */ + TEST_ASSERT((encoded & 0x80000000) != 0); + + double rel_err = fabs(decoded - val) / fabs(val); + TEST_ASSERT(rel_err < 1e-6); + } + TEST_PASS(); +} + +static void test_ieee_nearest_smaller() +{ + printf("Running %s ...\n", __func__); + double ret = 0; + int err = 0; + + double test_values[] = {0.0, 1.0, -1.0, 1.5, 273.15, -50.0, 0.001}; + + for (int i = 0; i < 7; i++) { + err = grib_nearest_smaller_ieee_float(test_values[i], &ret); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(ret <= test_values[i]); + } + + err = grib_nearest_smaller_ieee_float(0.0, &ret); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(ret == 0.0); + + TEST_PASS(); +} + +static void test_ieee_error_function() +{ + printf("Running %s ...\n", __func__); + double err_val = grib_ieeefloat_error(100.0); + TEST_ASSERT(err_val > 0.0); + + double err_small = grib_ieeefloat_error(1.0); + double err_large = grib_ieeefloat_error(1e6); + TEST_ASSERT(err_large >= err_small); + + TEST_PASS(); +} + +/* ========== Cross-format consistency ========== */ + +static void test_ibm_ieee_consistency() +{ + printf("Running %s ...\n", __func__); + + /* Both IBM and IEEE representations of simple values should decode + * to approximately the same value */ + double test_values[] = {0.0, 1.0, -1.0, 100.0, 273.15}; + + for (int i = 0; i < 5; i++) { + double val = test_values[i]; + double ibm_decoded = grib_long_to_ibm(grib_ibm_to_long(val)); + double ieee_decoded = grib_long_to_ieee(grib_ieee_to_long(val)); + + /* Both should be within 1e-5 of the original */ + TEST_ASSERT(fabs(ibm_decoded - val) < 1e-4 || val == 0.0); + TEST_ASSERT(fabs(ieee_decoded - val) < 1e-4 || val == 0.0); + } + + TEST_PASS(); +} + +/* ========== IBM/IEEE underflow ========== */ + +static void test_ibm_underflow() +{ + printf("Running %s ...\n", __func__); + /* Very small positive numbers should encode without error */ + unsigned long encoded = grib_ibm_to_long(1e-90); + /* Underflow should map to zero-like value */ + double decoded = grib_long_to_ibm(encoded); + TEST_ASSERT(decoded >= 0.0); + TEST_PASS(); +} + +static void test_ieee_underflow() +{ + printf("Running %s ...\n", __func__); + unsigned long encoded = grib_ieee_to_long(1e-90); + double decoded = grib_long_to_ieee(encoded); + TEST_ASSERT(decoded >= 0.0); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running float conversion unit tests...\n\n"); + + test_ibm_roundtrip_zero(); + test_ibm_roundtrip_positive(); + test_ibm_roundtrip_negative(); + test_ibm_nearest_smaller(); + test_ibm_error_function(); + test_ibm_underflow(); + + test_ieee_roundtrip_zero(); + test_ieee_roundtrip_positive(); + test_ieee_roundtrip_negative(); + test_ieee_nearest_smaller(); + test_ieee_error_function(); + test_ieee_underflow(); + + test_ibm_ieee_consistency(); + + printf("\n========================================\n"); + printf("Float conversion tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_geo.cc b/tests/test_geo.cc new file mode 100644 index 000000000..a6bd47668 --- /dev/null +++ b/tests/test_geo.cc @@ -0,0 +1,234 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for geo iterators and nearest-neighbor search. + * + * Tests geoiterator creation, traversal, point counting, and nearest-point + * lookup for the regular_ll grid type from GRIB2 samples. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: geoiterator creation and basic traversal ========== */ +static void test_iterator_regular_ll() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Set up data values so the iterator has something to traverse */ + size_t numValues = 0; + err = grib_get_size(h, "values", &numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(numValues > 0); + + double* vals = (double*)calloc(numValues, sizeof(double)); + TEST_ASSERT(vals != NULL); + for (size_t i = 0; i < numValues; i++) { + vals[i] = 273.15 + (double)(i % 50); + } + err = grib_set_double_array(h, "values", vals, numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Create iterator */ + grib_iterator* iter = grib_iterator_new(h, 0, &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(iter != NULL); + + /* Traverse all points */ + double lat = 0, lon = 0, value = 0; + long count = 0; + while (grib_iterator_next(iter, &lat, &lon, &value)) { + /* Latitude should be in [-90, 90] */ + TEST_ASSERT(lat >= -90.001 && lat <= 90.001); + /* Longitude should be in [0, 360] or [-180, 180] range */ + TEST_ASSERT(lon >= -180.001 && lon <= 360.001); + count++; + } + + /* Point count should match numberOfPoints */ + long expectedPoints = 0; + err = grib_get_long(h, "numberOfPoints", &expectedPoints); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(count == expectedPoints); + TEST_ASSERT((size_t)count == numValues); + + grib_iterator_delete(iter); + free(vals); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: iterator first/last point matches grid metadata ========== */ +static void test_iterator_endpoints() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Get expected first and last grid points from metadata */ + double latFirst = 0, lonFirst = 0, latLast = 0, lonLast = 0; + err = grib_get_double(h, "latitudeOfFirstGridPointInDegrees", &latFirst); + TEST_ASSERT(err == GRIB_SUCCESS); + err = grib_get_double(h, "longitudeOfFirstGridPointInDegrees", &lonFirst); + TEST_ASSERT(err == GRIB_SUCCESS); + err = grib_get_double(h, "latitudeOfLastGridPointInDegrees", &latLast); + TEST_ASSERT(err == GRIB_SUCCESS); + err = grib_get_double(h, "longitudeOfLastGridPointInDegrees", &lonLast); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Set some data */ + size_t numValues = 0; + err = grib_get_size(h, "values", &numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + double* vals = (double*)calloc(numValues, sizeof(double)); + for (size_t i = 0; i < numValues; i++) vals[i] = 1.0; + err = grib_set_double_array(h, "values", vals, numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Create iterator and check first point */ + grib_iterator* iter = grib_iterator_new(h, 0, &err); + TEST_ASSERT(err == GRIB_SUCCESS && iter != NULL); + + double lat = 0, lon = 0, value = 0; + int has_next = grib_iterator_next(iter, &lat, &lon, &value); + TEST_ASSERT(has_next); + TEST_ASSERT(fabs(lat - latFirst) < 0.01); + TEST_ASSERT(fabs(lon - lonFirst) < 0.01); + + /* Skip to last point */ + double lastLat = lat, lastLon = lon; + while (grib_iterator_next(iter, &lat, &lon, &value)) { + lastLat = lat; + lastLon = lon; + } + TEST_ASSERT(fabs(lastLat - latLast) < 0.01); + TEST_ASSERT(fabs(lastLon - lonLast) < 0.01); + + grib_iterator_delete(iter); + free(vals); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: nearest-neighbor search ========== */ +static void test_nearest_regular_ll() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Set data values */ + size_t numValues = 0; + err = grib_get_size(h, "values", &numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + double* vals = (double*)calloc(numValues, sizeof(double)); + for (size_t i = 0; i < numValues; i++) vals[i] = (double)i; + err = grib_set_double_array(h, "values", vals, numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Find nearest point to (45.0, 10.0) */ + grib_nearest* nearest = grib_nearest_new(h, &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(nearest != NULL); + + double inlat = 45.0, inlon = 10.0; + double outLats[4], outLons[4], outValues[4], outDistances[4]; + int outIndexes[4]; + size_t len = 4; + + err = grib_nearest_find(nearest, h, inlat, inlon, 0, + outLats, outLons, outValues, outDistances, outIndexes, &len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(len == 4); + + /* All returned points should be reasonably close to the input */ + for (size_t i = 0; i < len; i++) { + TEST_ASSERT(fabs(outLats[i] - inlat) < 5.0); /* within 5 degrees */ + TEST_ASSERT(outDistances[i] < 500.0); /* within 500 km (rough check) */ + TEST_ASSERT(outIndexes[i] >= 0); + TEST_ASSERT((size_t)outIndexes[i] < numValues); + } + + grib_nearest_delete(nearest); + free(vals); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: iterator with NO_VALUES flag ========== */ +static void test_iterator_no_values() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Create iterator with GRIB_GEOITERATOR_NO_VALUES flag */ + grib_iterator* iter = grib_iterator_new(h, GRIB_GEOITERATOR_NO_VALUES, &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(iter != NULL); + + double lat = 0, lon = 0, value = 0; + long count = 0; + while (grib_iterator_next(iter, &lat, &lon, &value)) { + TEST_ASSERT(lat >= -90.001 && lat <= 90.001); + count++; + } + TEST_ASSERT(count > 0); + + grib_iterator_delete(iter); + grib_handle_delete(h); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running geo iterator and nearest unit tests...\n\n"); + + test_iterator_regular_ll(); + test_iterator_endpoints(); + test_nearest_regular_ll(); + test_iterator_no_values(); + + printf("\n========================================\n"); + printf("Geo tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_io.cc b/tests/test_io.cc new file mode 100644 index 000000000..be921ef28 --- /dev/null +++ b/tests/test_io.cc @@ -0,0 +1,304 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for I/O functions and edge cases (grib_io.cc). + * + * Tests reading from memory buffers, counting messages, handling + * of truncated/corrupted data, and the multi-message API. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: get message from handle and re-create ========== */ +static void test_get_message_roundtrip() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + /* Create a GRIB message from sample */ + grib_handle* h1 = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h1 != NULL); + + /* Get the raw message bytes */ + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h1, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(mesg != NULL); + TEST_ASSERT(mesg_len > 0); + + /* Verify GRIB magic number at start */ + const unsigned char* bytes = (const unsigned char*)mesg; + TEST_ASSERT(bytes[0] == 'G'); + TEST_ASSERT(bytes[1] == 'R'); + TEST_ASSERT(bytes[2] == 'I'); + TEST_ASSERT(bytes[3] == 'B'); + + /* Verify 7777 terminator */ + TEST_ASSERT(bytes[mesg_len - 4] == '7'); + TEST_ASSERT(bytes[mesg_len - 3] == '7'); + TEST_ASSERT(bytes[mesg_len - 2] == '7'); + TEST_ASSERT(bytes[mesg_len - 1] == '7'); + + /* Create a new handle from the raw bytes */ + grib_handle* h2 = grib_handle_new_from_message(NULL, mesg, mesg_len); + TEST_ASSERT(h2 != NULL); + + /* Verify key values match */ + long edition1 = 0, edition2 = 0; + grib_get_long(h1, "edition", &edition1); + grib_get_long(h2, "edition", &edition2); + TEST_ASSERT(edition1 == edition2); + + grib_handle_delete(h2); + grib_handle_delete(h1); + TEST_PASS(); +} + +/* ========== Test: count messages in file ========== */ +static void test_count_in_file() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + /* Create a temporary file with one GRIB message */ + char tmpfile[] = "temp_test_io_count.grib"; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + + FILE* fp = fopen(tmpfile, "wb"); + TEST_ASSERT(fp != NULL); + fwrite(mesg, 1, mesg_len, fp); + /* Write same message again */ + fwrite(mesg, 1, mesg_len, fp); + fclose(fp); + + /* Count messages */ + fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + int n = 0; + err = grib_count_in_file(NULL, fp, &n); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(n == 2); + fclose(fp); + + grib_handle_delete(h); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: reading from empty file ========== */ +static void test_empty_file() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + char tmpfile[] = "temp_test_io_empty.grib"; + FILE* fp = fopen(tmpfile, "wb"); + TEST_ASSERT(fp != NULL); + fclose(fp); + + fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + int n = 0; + err = grib_count_in_file(NULL, fp, &n); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(n == 0); + fclose(fp); + + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: truncated GRIB message ========== */ +static void test_truncated_message() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + /* Create a valid GRIB message */ + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(mesg_len > 16); + + /* Write a truncated message (cut off last half) */ + char tmpfile[] = "temp_test_io_truncated.grib"; + FILE* fp = fopen(tmpfile, "wb"); + TEST_ASSERT(fp != NULL); + size_t half = mesg_len / 2; + fwrite(mesg, 1, half, fp); + fclose(fp); + + /* Try to read from truncated file - should fail or return 0 messages */ + fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + + grib_handle* h2 = grib_handle_new_from_file(NULL, fp, &err); + /* Either returns NULL handle or an error code */ + if (h2 != NULL) { + grib_handle_delete(h2); + } + /* err should indicate a problem (at minimum not a successful full read) */ + fclose(fp); + + grib_handle_delete(h); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: corrupted magic number ========== */ +static void test_corrupted_magic() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Copy and corrupt the magic number */ + unsigned char* corrupt = (unsigned char*)malloc(mesg_len); + TEST_ASSERT(corrupt != NULL); + memcpy(corrupt, mesg, mesg_len); + corrupt[0] = 'X'; /* Corrupt 'G' -> 'X' */ + + char tmpfile[] = "temp_test_io_corrupt.grib"; + FILE* fp = fopen(tmpfile, "wb"); + TEST_ASSERT(fp != NULL); + fwrite(corrupt, 1, mesg_len, fp); + fclose(fp); + + /* Try to read - should fail */ + fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + int n = 0; + err = grib_count_in_file(NULL, fp, &n); + /* A corrupted file should yield 0 valid messages */ + TEST_ASSERT(n == 0); + fclose(fp); + + free(corrupt); + grib_handle_delete(h); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: GRIB message from copy ========== */ +static void test_handle_new_from_message_copy() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h1 = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h1 != NULL); + + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h1, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* grib_handle_new_from_message_copy makes its own copy of the buffer */ + grib_handle* h2 = grib_handle_new_from_message_copy(NULL, mesg, mesg_len); + TEST_ASSERT(h2 != NULL); + + long edition = 0; + err = grib_get_long(h2, "edition", &edition); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition == 2); + + grib_handle_delete(h2); + grib_handle_delete(h1); + TEST_PASS(); +} + +/* ========== Test: BUFR sample creation ========== */ +static void test_bufr_sample_creation() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(h != NULL); + + long edition = 0; + err = grib_get_long(h, "edition", &edition); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition == 4); + + /* Get message bytes */ + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(mesg_len > 0); + + /* Verify BUFR magic number */ + const unsigned char* bytes = (const unsigned char*)mesg; + TEST_ASSERT(bytes[0] == 'B'); + TEST_ASSERT(bytes[1] == 'U'); + TEST_ASSERT(bytes[2] == 'F'); + TEST_ASSERT(bytes[3] == 'R'); + + grib_handle_delete(h); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running I/O unit tests...\n\n"); + + test_get_message_roundtrip(); + test_count_in_file(); + test_empty_file(); + test_truncated_message(); + test_corrupted_magic(); + test_handle_new_from_message_copy(); + test_bufr_sample_creation(); + + printf("\n========================================\n"); + printf("I/O tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_unit_extended.sh b/tests/test_unit_extended.sh new file mode 100755 index 000000000..65779cfb0 --- /dev/null +++ b/tests/test_unit_extended.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# (C) Copyright 2005- ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# +# In applying this licence, ECMWF does not waive the privileges and immunities granted to it by +# virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. +# + +. ./include.ctest.sh + +# Wrapper script for C++ unit test executables + +echo "Running test_bits..." +$EXEC ${test_dir}/test_bits +echo "" + +echo "Running test_float_conversions..." +$EXEC ${test_dir}/test_float_conversions +echo "" + +echo "Running test_value_api..." +$EXEC ${test_dir}/test_value_api +echo "" + +echo "Running test_expression..." +$EXEC ${test_dir}/test_expression +echo "" + +echo "Running test_data_structures..." +$EXEC ${test_dir}/test_data_structures +echo "" + +echo "Running test_geo..." +$EXEC ${test_dir}/test_geo +echo "" + +echo "Running test_io..." +$EXEC ${test_dir}/test_io +echo "" + +echo "All new unit tests passed." diff --git a/tests/test_value_api.cc b/tests/test_value_api.cc new file mode 100644 index 000000000..ad6b3f366 --- /dev/null +++ b/tests/test_value_api.cc @@ -0,0 +1,348 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for the core get/set API (grib_value.cc). + * + * Tests grib_get_long, grib_set_long, grib_get_double, grib_set_double, + * grib_get_string, grib_set_string, grib_is_missing, grib_is_defined, + * grib_get_size, grib_set_double_array, error returns for invalid inputs. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: get/set long ========== */ +static void test_get_set_long() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Set and get edition */ + long edition = 0; + err = grib_get_long(h, "edition", &edition); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition == 2); + + /* Set paramId */ + err = grib_set_long(h, "paramId", 167); + TEST_ASSERT(err == GRIB_SUCCESS); + + long paramId = 0; + err = grib_get_long(h, "paramId", ¶mId); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(paramId == 167); + + /* Set Ni and Nj */ + err = grib_set_long(h, "Ni", 360); + TEST_ASSERT(err == GRIB_SUCCESS); + err = grib_set_long(h, "Nj", 181); + TEST_ASSERT(err == GRIB_SUCCESS); + + long Ni = 0; + err = grib_get_long(h, "Ni", &Ni); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(Ni == 360); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: get/set double ========== */ +static void test_get_set_double() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Set and get latitudeOfFirstGridPointInDegrees */ + err = grib_set_double(h, "latitudeOfFirstGridPointInDegrees", 90.0); + TEST_ASSERT(err == GRIB_SUCCESS); + + double lat = 0; + err = grib_get_double(h, "latitudeOfFirstGridPointInDegrees", &lat); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(fabs(lat - 90.0) < 1e-6); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: get/set string ========== */ +static void test_get_set_string() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Get gridType (should be "regular_ll" for default GRIB2 sample) */ + char gridType[64] = {0}; + size_t len = sizeof(gridType); + err = grib_get_string(h, "gridType", gridType, &len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(strlen(gridType) > 0); + + /* Get packingType */ + char packingType[64] = {0}; + len = sizeof(packingType); + err = grib_get_string(h, "packingType", packingType, &len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(strlen(packingType) > 0); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: invalid key returns GRIB_NOT_FOUND ========== */ +static void test_invalid_key() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + long val = 0; + err = grib_get_long(h, "thisKeyDoesNotExistAtAll", &val); + TEST_ASSERT(err == GRIB_NOT_FOUND); + + double dval = 0; + err = grib_get_double(h, "thisKeyDoesNotExistAtAll", &dval); + TEST_ASSERT(err == GRIB_NOT_FOUND); + + char sval[64] = {0}; + size_t slen = sizeof(sval); + err = grib_get_string(h, "thisKeyDoesNotExistAtAll", sval, &slen); + TEST_ASSERT(err == GRIB_NOT_FOUND); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: grib_is_defined ========== */ +static void test_is_defined() +{ + printf("Running %s ...\n", __func__); + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* "edition" should be defined */ + TEST_ASSERT(grib_is_defined(h, "edition") == 1); + + /* A non-existent key should not be defined */ + TEST_ASSERT(grib_is_defined(h, "nonExistentKeyXYZ") == 0); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: grib_get_size ========== */ +static void test_get_size() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + size_t size = 0; + err = grib_get_size(h, "values", &size); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(size > 0); + + /* edition has size 1 */ + err = grib_get_size(h, "edition", &size); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(size == 1); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: set/get double array ========== */ +static void test_set_get_double_array() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Get the expected number of values */ + size_t numValues = 0; + err = grib_get_size(h, "values", &numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(numValues > 0); + + /* Create and set an array of values */ + double* vals = (double*)calloc(numValues, sizeof(double)); + TEST_ASSERT(vals != NULL); + + for (size_t i = 0; i < numValues; i++) { + vals[i] = 273.15 + (double)(i % 100) * 0.1; /* Temperature-like values */ + } + + err = grib_set_double_array(h, "values", vals, numValues); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Read back */ + double* read_vals = (double*)calloc(numValues, sizeof(double)); + TEST_ASSERT(read_vals != NULL); + size_t read_count = numValues; + err = grib_get_double_array(h, "values", read_vals, &read_count); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(read_count == numValues); + + /* Verify values are close (packing introduces small errors) */ + double maxErr = 0; + for (size_t i = 0; i < numValues; i++) { + double diff = fabs(read_vals[i] - vals[i]); + if (diff > maxErr) maxErr = diff; + } + /* Simple packing should be accurate to within bitsPerValue precision */ + TEST_ASSERT(maxErr < 0.01); + + free(vals); + free(read_vals); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: read-only key protection ========== */ +static void test_read_only_key() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* "7777" is a read-only key (end marker) */ + err = grib_set_long(h, "7777", 999); + TEST_ASSERT(err != GRIB_SUCCESS); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: handle cloning ========== */ +static void test_handle_clone() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + err = grib_set_long(h, "paramId", 130); + TEST_ASSERT(err == GRIB_SUCCESS); + + grib_handle* clone = grib_handle_clone(h); + TEST_ASSERT(clone != NULL); + + long paramId = 0; + err = grib_get_long(clone, "paramId", ¶mId); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(paramId == 130); + + /* Modifying clone should not affect original */ + err = grib_set_long(clone, "paramId", 167); + TEST_ASSERT(err == GRIB_SUCCESS); + + err = grib_get_long(h, "paramId", ¶mId); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(paramId == 130); + + grib_handle_delete(clone); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: GRIB1 sample ========== */ +static void test_grib1_sample() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB1"); + TEST_ASSERT(h != NULL); + + long edition = 0; + err = grib_get_long(h, "edition", &edition); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition == 1); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: get native type ========== */ +static void test_get_native_type() +{ + printf("Running %s ...\n", __func__); + int err = 0; + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + int type = 0; + err = grib_get_native_type(h, "edition", &type); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(type == GRIB_TYPE_LONG); + + err = grib_get_native_type(h, "values", &type); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(type == GRIB_TYPE_DOUBLE); + + err = grib_get_native_type(h, "gridType", &type); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(type == GRIB_TYPE_STRING); + + grib_handle_delete(h); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running core value API unit tests...\n\n"); + + test_get_set_long(); + test_get_set_double(); + test_get_set_string(); + test_invalid_key(); + test_is_defined(); + test_get_size(); + test_set_get_double_array(); + test_read_only_key(); + test_handle_clone(); + test_grib1_sample(); + test_get_native_type(); + + printf("\n========================================\n"); + printf("Value API tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} From 8f6f5738f8ecb31401336461dd505b113a51e80f Mon Sep 17 00:00:00 2001 From: Tiago Quintino Date: Wed, 11 Mar 2026 09:16:34 +0000 Subject: [PATCH 2/6] More tests, try to complete coverage --- tests/CMakeLists.txt | 6 +++++- tests/test_unit_extended.sh | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3b5f8fad8..67a8f49bc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -82,7 +82,11 @@ list(APPEND test_c_bins test_expression test_data_structures test_geo - test_io) + test_io + test_trie + test_keys_iterator + test_handle_lifecycle + test_util) foreach( tool ${test_c_bins} ) diff --git a/tests/test_unit_extended.sh b/tests/test_unit_extended.sh index 65779cfb0..685b9d5d4 100755 --- a/tests/test_unit_extended.sh +++ b/tests/test_unit_extended.sh @@ -40,4 +40,20 @@ echo "Running test_io..." $EXEC ${test_dir}/test_io echo "" +echo "Running test_trie..." +$EXEC ${test_dir}/test_trie +echo "" + +echo "Running test_keys_iterator..." +$EXEC ${test_dir}/test_keys_iterator +echo "" + +echo "Running test_handle_lifecycle..." +$EXEC ${test_dir}/test_handle_lifecycle +echo "" + +echo "Running test_util..." +$EXEC ${test_dir}/test_util +echo "" + echo "All new unit tests passed." From 8322d77af98ca8702aeb8a1bd2aedd46d8506c85 Mon Sep 17 00:00:00 2001 From: Tiago Quintino Date: Wed, 11 Mar 2026 09:26:57 +0000 Subject: [PATCH 3/6] More tests, try to complete coverage --- tests/test_handle_lifecycle.cc | 282 +++++++++++++++++++++++++++++++++ tests/test_keys_iterator.cc | 213 +++++++++++++++++++++++++ tests/test_trie.cc | 261 ++++++++++++++++++++++++++++++ tests/test_util.cc | 183 +++++++++++++++++++++ 4 files changed, 939 insertions(+) create mode 100644 tests/test_handle_lifecycle.cc create mode 100644 tests/test_keys_iterator.cc create mode 100644 tests/test_trie.cc create mode 100644 tests/test_util.cc diff --git a/tests/test_handle_lifecycle.cc b/tests/test_handle_lifecycle.cc new file mode 100644 index 000000000..516806377 --- /dev/null +++ b/tests/test_handle_lifecycle.cc @@ -0,0 +1,282 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for handle lifecycle functions (grib_handle.cc): + * - codes_check_message_header / codes_check_message_footer + * - codes_get_product_kind + * - grib_handle_clone_headers_only + * - grib_get_message_size + * - grib_multi_handle (new / append / write / delete) + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: codes_check_message_header ========== */ +static void test_check_message_header() +{ + printf("Running %s ...\n", __func__); + + /* Valid GRIB header */ + const char grib_header[] = "GRIB____"; + int err = codes_check_message_header(grib_header, 8, PRODUCT_GRIB); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Valid BUFR header */ + const char bufr_header[] = "BUFR____"; + err = codes_check_message_header(bufr_header, 8, PRODUCT_BUFR); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Wrong product type for these bytes */ + err = codes_check_message_header(grib_header, 8, PRODUCT_BUFR); + TEST_ASSERT(err != GRIB_SUCCESS); + + err = codes_check_message_header(bufr_header, 8, PRODUCT_GRIB); + TEST_ASSERT(err != GRIB_SUCCESS); + + /* Invalid header */ + const char bad_header[] = "XXXX____"; + err = codes_check_message_header(bad_header, 8, PRODUCT_GRIB); + TEST_ASSERT(err != GRIB_SUCCESS); + + TEST_PASS(); +} + +/* ========== Test: codes_check_message_footer ========== */ +static void test_check_message_footer() +{ + printf("Running %s ...\n", __func__); + + const char good_footer[] = "7777"; + int err = codes_check_message_footer(good_footer, 4, PRODUCT_GRIB); + TEST_ASSERT(err == GRIB_SUCCESS); + + err = codes_check_message_footer(good_footer, 4, PRODUCT_BUFR); + TEST_ASSERT(err == GRIB_SUCCESS); + + const char bad_footer[] = "7778"; + err = codes_check_message_footer(bad_footer, 4, PRODUCT_GRIB); + TEST_ASSERT(err != GRIB_SUCCESS); + + const char zeros[] = "\0\0\0\0"; + err = codes_check_message_footer(zeros, 4, PRODUCT_GRIB); + TEST_ASSERT(err != GRIB_SUCCESS); + + TEST_PASS(); +} + +/* ========== Test: codes_get_product_kind ========== */ +static void test_get_product_kind() +{ + printf("Running %s ...\n", __func__); + int err = 0; + ProductKind kind; + + /* GRIB sample */ + grib_handle* hg = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(hg != NULL); + err = codes_get_product_kind(hg, &kind); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(kind == PRODUCT_GRIB); + grib_handle_delete(hg); + + /* BUFR sample */ + grib_handle* hb = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(hb != NULL); + err = codes_get_product_kind(hb, &kind); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(kind == PRODUCT_BUFR); + grib_handle_delete(hb); + + TEST_PASS(); +} + +/* ========== Test: grib_handle_clone_headers_only ========== */ +static void test_clone_headers_only() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_handle* hc = grib_handle_clone_headers_only(h); + TEST_ASSERT(hc != NULL); + + /* Metadata keys should match */ + long ed_orig = 0, ed_clone = 0; + err = grib_get_long(h, "edition", &ed_orig); + TEST_ASSERT(err == GRIB_SUCCESS); + err = grib_get_long(hc, "edition", &ed_clone); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(ed_orig == ed_clone); + + /* The clone's message should be smaller (no data section) */ + const void* mesg_orig = NULL; + size_t len_orig = 0; + grib_get_message(h, &mesg_orig, &len_orig); + + const void* mesg_clone = NULL; + size_t len_clone = 0; + grib_get_message(hc, &mesg_clone, &len_clone); + + printf(" Original size: %zu, headers-only clone size: %zu\n", len_orig, len_clone); + TEST_ASSERT(len_clone <= len_orig); + + grib_handle_delete(hc); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: grib_get_message_size ========== */ +static void test_get_message_size() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Get size via grib_get_message_size */ + size_t size = 0; + err = grib_get_message_size(h, &size); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(size > 0); + + /* Get size via grib_get_message and compare */ + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h, &mesg, &mesg_len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(size == mesg_len); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: multi-handle lifecycle ========== */ +static void test_multi_handle_lifecycle() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_multi_handle* mh = grib_multi_handle_new(NULL); + TEST_ASSERT(mh != NULL); + + /* Create two different GRIB messages */ + grib_handle* h1 = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h1 != NULL); + err = grib_set_long(h1, "paramId", 130); /* Temperature */ + TEST_ASSERT(err == GRIB_SUCCESS); + + grib_handle* h2 = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h2 != NULL); + err = grib_set_long(h2, "paramId", 131); /* U-wind */ + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Append both to multi-handle */ + err = grib_multi_handle_append(h1, 0, mh); + TEST_ASSERT(err == GRIB_SUCCESS); + err = grib_multi_handle_append(h2, 0, mh); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Write to temp file */ + const char* tmpfile = "temp_test_multi_handle.grib"; + FILE* fp = fopen(tmpfile, "wb"); + TEST_ASSERT(fp != NULL); + err = grib_multi_handle_write(mh, fp); + TEST_ASSERT(err == GRIB_SUCCESS); + fclose(fp); + + /* Count messages in written file */ + fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + int n = 0; + err = grib_count_in_file(NULL, fp, &n); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(n == 2); + + /* Read back and verify paramIds */ + rewind(fp); + grib_handle* hr1 = grib_handle_new_from_file(NULL, fp, &err); + TEST_ASSERT(hr1 != NULL); + long paramId1 = 0; + grib_get_long(hr1, "paramId", ¶mId1); + TEST_ASSERT(paramId1 == 130); + + grib_handle* hr2 = grib_handle_new_from_file(NULL, fp, &err); + TEST_ASSERT(hr2 != NULL); + long paramId2 = 0; + grib_get_long(hr2, "paramId", ¶mId2); + TEST_ASSERT(paramId2 == 131); + + grib_handle_delete(hr2); + grib_handle_delete(hr1); + fclose(fp); + + grib_handle_delete(h2); + grib_handle_delete(h1); + grib_multi_handle_delete(mh); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: multi-handle delete on empty handle ========== */ +static void test_multi_handle_empty() +{ + printf("Running %s ...\n", __func__); + + grib_multi_handle* mh = grib_multi_handle_new(NULL); + TEST_ASSERT(mh != NULL); + + /* Delete without appending anything — should not crash */ + int err = grib_multi_handle_delete(mh); + TEST_ASSERT(err == GRIB_SUCCESS); + + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running handle lifecycle unit tests...\n\n"); + + test_check_message_header(); + test_check_message_footer(); + test_get_product_kind(); + test_clone_headers_only(); + test_get_message_size(); + test_multi_handle_lifecycle(); + test_multi_handle_empty(); + + printf("\n========================================\n"); + printf("Handle lifecycle tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_keys_iterator.cc b/tests/test_keys_iterator.cc new file mode 100644 index 000000000..461956149 --- /dev/null +++ b/tests/test_keys_iterator.cc @@ -0,0 +1,213 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for the keys iterator API (grib_keys_iterator.cc). + * + * Tests basic lifecycle, rewind, namespace filtering, skip flags, + * and finding specific keys during iteration. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: basic lifecycle ========== */ +static void test_basic_lifecycle() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_keys_iterator* kiter = grib_keys_iterator_new(h, 0, NULL); + TEST_ASSERT(kiter != NULL); + + int count = 0; + while (grib_keys_iterator_next(kiter)) { + const char* name = grib_keys_iterator_get_name(kiter); + TEST_ASSERT(name != NULL); + TEST_ASSERT(strlen(name) > 0); + count++; + } + TEST_ASSERT(count > 0); + printf(" Total keys (no filter): %d\n", count); + + grib_keys_iterator_delete(kiter); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: rewind gives same count ========== */ +static void test_rewind() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_keys_iterator* kiter = grib_keys_iterator_new(h, 0, NULL); + TEST_ASSERT(kiter != NULL); + + /* First pass */ + int count1 = 0; + while (grib_keys_iterator_next(kiter)) { + count1++; + } + TEST_ASSERT(count1 > 0); + + /* Rewind */ + int err = grib_keys_iterator_rewind(kiter); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Second pass */ + int count2 = 0; + while (grib_keys_iterator_next(kiter)) { + count2++; + } + + TEST_ASSERT(count1 == count2); + + grib_keys_iterator_delete(kiter); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: namespace filter reduces keys ========== */ +static void test_namespace_filter() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* Iterate all keys */ + grib_keys_iterator* kiter_all = grib_keys_iterator_new(h, 0, NULL); + TEST_ASSERT(kiter_all != NULL); + int count_all = 0; + while (grib_keys_iterator_next(kiter_all)) { + count_all++; + } + grib_keys_iterator_delete(kiter_all); + + /* Iterate only the "ls" namespace */ + grib_keys_iterator* kiter_ls = grib_keys_iterator_new(h, 0, "ls"); + TEST_ASSERT(kiter_ls != NULL); + int count_ls = 0; + while (grib_keys_iterator_next(kiter_ls)) { + count_ls++; + } + grib_keys_iterator_delete(kiter_ls); + + /* "ls" namespace should have fewer keys than all */ + printf(" All keys: %d, 'ls' namespace: %d\n", count_all, count_ls); + TEST_ASSERT(count_ls > 0); + TEST_ASSERT(count_ls < count_all); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: skip flags reduce key count ========== */ +static void test_skip_flags() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + /* No flags */ + grib_keys_iterator* kiter_none = grib_keys_iterator_new(h, 0, NULL); + TEST_ASSERT(kiter_none != NULL); + int count_none = 0; + while (grib_keys_iterator_next(kiter_none)) { + count_none++; + } + grib_keys_iterator_delete(kiter_none); + + /* Skip read-only + computed + duplicates */ + unsigned long flags = GRIB_KEYS_ITERATOR_SKIP_READ_ONLY | + GRIB_KEYS_ITERATOR_SKIP_COMPUTED | + GRIB_KEYS_ITERATOR_SKIP_DUPLICATES; + grib_keys_iterator* kiter_skip = grib_keys_iterator_new(h, flags, NULL); + TEST_ASSERT(kiter_skip != NULL); + int count_skip = 0; + while (grib_keys_iterator_next(kiter_skip)) { + count_skip++; + } + grib_keys_iterator_delete(kiter_skip); + + /* Skipping should yield fewer keys */ + printf(" No flags: %d, with skip flags: %d\n", count_none, count_skip); + TEST_ASSERT(count_skip < count_none); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: "editionNumber" key is found during iteration ========== */ +static void test_find_edition_key() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = grib_handle_new_from_samples(NULL, "GRIB2"); + TEST_ASSERT(h != NULL); + + grib_keys_iterator* kiter = grib_keys_iterator_new(h, 0, NULL); + TEST_ASSERT(kiter != NULL); + + /* Note: "edition" is an alias; the underlying key is "editionNumber" */ + int found_edition = 0; + while (grib_keys_iterator_next(kiter)) { + const char* name = grib_keys_iterator_get_name(kiter); + if (name && strcmp(name, "editionNumber") == 0) { + found_edition = 1; + break; + } + } + TEST_ASSERT(found_edition == 1); + + grib_keys_iterator_delete(kiter); + grib_handle_delete(h); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running keys iterator unit tests...\n\n"); + + test_basic_lifecycle(); + test_rewind(); + test_namespace_filter(); + test_skip_flags(); + test_find_edition_key(); + + printf("\n========================================\n"); + printf("Keys iterator tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_trie.cc b/tests/test_trie.cc new file mode 100644 index 000000000..e429ed50b --- /dev/null +++ b/tests/test_trie.cc @@ -0,0 +1,261 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for the internal grib_trie data structure (grib_trie.cc). + * + * Tests insert/get round-trips, overwrite semantics, insert_no_replace, + * missing key lookup, clear, many-key growth, and special characters. + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: basic insert and get ========== */ +static void test_insert_get() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + int val1 = 42; + int val2 = 99; + + /* Insert and retrieve */ + void* old = grib_trie_insert(t, "edition", &val1); + TEST_ASSERT(old == NULL); /* no previous value */ + + void* got = grib_trie_get(t, "edition"); + TEST_ASSERT(got == &val1); + TEST_ASSERT(*(int*)got == 42); + + /* Insert a second key */ + old = grib_trie_insert(t, "step", &val2); + TEST_ASSERT(old == NULL); + + got = grib_trie_get(t, "step"); + TEST_ASSERT(got == &val2); + TEST_ASSERT(*(int*)got == 99); + + /* First key still there */ + got = grib_trie_get(t, "edition"); + TEST_ASSERT(got == &val1); + + grib_trie_delete_container(t); + TEST_PASS(); +} + +/* ========== Test: overwrite returns old value ========== */ +static void test_overwrite() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + int val1 = 10; + int val2 = 20; + + grib_trie_insert(t, "paramId", &val1); + void* old = grib_trie_insert(t, "paramId", &val2); + TEST_ASSERT(old == &val1); /* returns the old value */ + + void* got = grib_trie_get(t, "paramId"); + TEST_ASSERT(got == &val2); + TEST_ASSERT(*(int*)got == 20); + + grib_trie_delete_container(t); + TEST_PASS(); +} + +/* ========== Test: insert_no_replace does not overwrite ========== */ +static void test_insert_no_replace() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + int val1 = 100; + int val2 = 200; + + /* First insert succeeds */ + void* result = grib_trie_insert_no_replace(t, "gridType", &val1); + TEST_ASSERT(result == &val1); + + /* Second insert with same key returns existing value, does NOT replace */ + result = grib_trie_insert_no_replace(t, "gridType", &val2); + TEST_ASSERT(result == &val1); /* still the original */ + + void* got = grib_trie_get(t, "gridType"); + TEST_ASSERT(got == &val1); + TEST_ASSERT(*(int*)got == 100); + + grib_trie_delete_container(t); + TEST_PASS(); +} + +/* ========== Test: get on missing key returns NULL ========== */ +static void test_missing_key() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + void* got = grib_trie_get(t, "nonexistent"); + TEST_ASSERT(got == NULL); + + /* Insert something, then check a different key */ + int val = 1; + grib_trie_insert(t, "level", &val); + got = grib_trie_get(t, "levels"); /* similar but different */ + TEST_ASSERT(got == NULL); + + got = grib_trie_get(t, "leve"); /* prefix of existing */ + TEST_ASSERT(got == NULL); + + grib_trie_delete_container(t); + TEST_PASS(); +} + +/* ========== Test: clear zeros all entries ========== */ +static void test_clear() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + int val1 = 1, val2 = 2, val3 = 3; + grib_trie_insert(t, "a", &val1); + grib_trie_insert(t, "b", &val2); + grib_trie_insert(t, "c", &val3); + + TEST_ASSERT(grib_trie_get(t, "a") == &val1); + TEST_ASSERT(grib_trie_get(t, "b") == &val2); + TEST_ASSERT(grib_trie_get(t, "c") == &val3); + + grib_trie_clear(t); + + /* All should be NULL now */ + TEST_ASSERT(grib_trie_get(t, "a") == NULL); + TEST_ASSERT(grib_trie_get(t, "b") == NULL); + TEST_ASSERT(grib_trie_get(t, "c") == NULL); + + /* Trie is still usable after clear */ + int val4 = 4; + grib_trie_insert(t, "a", &val4); + TEST_ASSERT(grib_trie_get(t, "a") == &val4); + + grib_trie_delete_container(t); + TEST_PASS(); +} + +/* ========== Test: many keys exercise trie growth ========== */ +static void test_many_keys() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + const int N = 200; + int values[200]; + char keys[200][32]; + + for (int i = 0; i < N; i++) { + values[i] = i * 10; + snprintf(keys[i], sizeof(keys[i]), "key_%d", i); + grib_trie_insert(t, keys[i], &values[i]); + } + + /* Verify all keys are retrievable */ + for (int i = 0; i < N; i++) { + void* got = grib_trie_get(t, keys[i]); + TEST_ASSERT(got != NULL); + TEST_ASSERT(*(int*)got == i * 10); + } + + grib_trie_delete_container(t); + TEST_PASS(); +} + +/* ========== Test: keys with special characters ========== */ +static void test_special_chars() +{ + printf("Running %s ...\n", __func__); + grib_context* c = grib_context_get_default(); + grib_trie* t = grib_trie_new(c); + TEST_ASSERT(t != NULL); + + int v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5; + + /* Keys with underscores (common in ecCodes keys) */ + grib_trie_insert(t, "grid_type", &v1); + TEST_ASSERT(grib_trie_get(t, "grid_type") == &v1); + + /* Keys with hash (used in BUFR rank: #1#pressure) */ + grib_trie_insert(t, "#1#pressure", &v2); + TEST_ASSERT(grib_trie_get(t, "#1#pressure") == &v2); + + /* Keys with digits */ + grib_trie_insert(t, "section3", &v3); + TEST_ASSERT(grib_trie_get(t, "section3") == &v3); + + /* Single character keys */ + grib_trie_insert(t, "x", &v4); + TEST_ASSERT(grib_trie_get(t, "x") == &v4); + + /* Uppercase */ + grib_trie_insert(t, "GRIB", &v5); + TEST_ASSERT(grib_trie_get(t, "GRIB") == &v5); + + grib_trie_delete_container(t); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running trie unit tests...\n\n"); + + test_insert_get(); + test_overwrite(); + test_insert_no_replace(); + test_missing_key(); + test_clear(); + test_many_keys(); + test_special_chars(); + + printf("\n========================================\n"); + printf("Trie tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_util.cc b/tests/test_util.cc new file mode 100644 index 000000000..fc927161a --- /dev/null +++ b/tests/test_util.cc @@ -0,0 +1,183 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for pure utility functions from grib_util.cc: + * - grib_get_reduced_row (non-legacy) + * - grib_get_reduced_row_p (double precision) + * - sum_of_pl_array + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* ========== Test: grib_get_reduced_row basic ========== */ +static void test_get_reduced_row_basic() +{ + printf("Running %s ...\n", __func__); + + long npoints = 0, ilon_first = 0, ilon_last = 0; + + /* Full globe: 0 to 360 with 36 points on this latitude circle */ + grib_get_reduced_row(36, 0.0, 360.0, &npoints, &ilon_first, &ilon_last); + TEST_ASSERT(npoints == 36); + TEST_ASSERT(ilon_first == 0); + + /* Subsection of the globe */ + grib_get_reduced_row(36, 0.0, 180.0, &npoints, &ilon_first, &ilon_last); + TEST_ASSERT(npoints > 0); + TEST_ASSERT(npoints <= 36); + + /* Small section */ + grib_get_reduced_row(100, 10.0, 20.0, &npoints, &ilon_first, &ilon_last); + TEST_ASSERT(npoints > 0); + TEST_ASSERT(npoints <= 100); + + TEST_PASS(); +} + +/* ========== Test: grib_get_reduced_row_p matches grib_get_reduced_row ========== */ +static void test_reduced_row_p_consistency() +{ + printf("Running %s ...\n", __func__); + + long npoints1 = 0, ilon_first1 = 0, ilon_last1 = 0; + long npoints2 = 0; + double olon_first2 = 0, olon_last2 = 0; + + /* Both functions should return the same npoints for same input */ + grib_get_reduced_row(48, 0.0, 100.0, &npoints1, &ilon_first1, &ilon_last1); + grib_get_reduced_row_p(48, 0.0, 100.0, &npoints2, &olon_first2, &olon_last2); + + TEST_ASSERT(npoints1 == npoints2); + TEST_ASSERT(npoints1 > 0); + + /* _p variant returns doubles; verify they are within the requested range */ + TEST_ASSERT(olon_first2 >= -0.001); /* small tolerance */ + TEST_ASSERT(olon_last2 <= 100.001); + + /* Test with wrap-around */ + grib_get_reduced_row(25, 350.0, 10.0, &npoints1, &ilon_first1, &ilon_last1); + grib_get_reduced_row_p(25, 350.0, 10.0, &npoints2, &olon_first2, &olon_last2); + TEST_ASSERT(npoints1 == npoints2); + + TEST_PASS(); +} + +/* ========== Test: grib_get_reduced_row edge cases ========== */ +static void test_reduced_row_edge_cases() +{ + printf("Running %s ...\n", __func__); + + long npoints = 0, ilon_first = 0, ilon_last = 0; + + /* Same lon: lon_first == lon_last */ + grib_get_reduced_row(36, 90.0, 90.0, &npoints, &ilon_first, &ilon_last); + /* Should return at least 1 point */ + TEST_ASSERT(npoints >= 1); + + /* Large pl value */ + grib_get_reduced_row(1000, 0.0, 359.0, &npoints, &ilon_first, &ilon_last); + TEST_ASSERT(npoints > 0); + TEST_ASSERT(npoints <= 1000); + + /* Very small subsection */ + grib_get_reduced_row(200, 0.0, 1.0, &npoints, &ilon_first, &ilon_last); + TEST_ASSERT(npoints > 0); + + TEST_PASS(); +} + +/* ========== Test: sum_of_pl_array basic ========== */ +static void test_sum_of_pl_array_basic() +{ + printf("Running %s ...\n", __func__); + + long pl1[] = {10, 20, 30, 40, 50}; + size_t sum = sum_of_pl_array(pl1, 5); + TEST_ASSERT(sum == 150); + + /* Single element */ + long pl2[] = {42}; + sum = sum_of_pl_array(pl2, 1); + TEST_ASSERT(sum == 42); + + /* All same values */ + long pl3[] = {8, 8, 8, 8}; + sum = sum_of_pl_array(pl3, 4); + TEST_ASSERT(sum == 32); + + /* Typical Gaussian pl array (O32 first few) */ + long pl4[] = {20, 25, 36, 40, 45, 50, 60, 64}; + sum = sum_of_pl_array(pl4, 8); + TEST_ASSERT(sum == 340); + + TEST_PASS(); +} + +/* ========== Test: sum_of_pl_array large ========== */ +static void test_sum_of_pl_array_large() +{ + printf("Running %s ...\n", __func__); + + /* Large array of 1000 elements, all = 100 */ + const int N = 1000; + long pl[1000]; + for (int i = 0; i < N; i++) { + pl[i] = 100; + } + size_t sum = sum_of_pl_array(pl, N); + TEST_ASSERT(sum == 100000); + + /* Ascending array */ + for (int i = 0; i < N; i++) { + pl[i] = i + 1; + } + sum = sum_of_pl_array(pl, N); + /* sum of 1..1000 = 1000*1001/2 = 500500 */ + TEST_ASSERT(sum == 500500); + + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running utility function unit tests...\n\n"); + + test_get_reduced_row_basic(); + test_reduced_row_p_consistency(); + test_reduced_row_edge_cases(); + test_sum_of_pl_array_basic(); + test_sum_of_pl_array_large(); + + printf("\n========================================\n"); + printf("Utility tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} From 76af7c3bbb3ac91eb795346d973f8d169f34ca4f Mon Sep 17 00:00:00 2001 From: Tiago Quintino Date: Wed, 11 Mar 2026 09:48:23 +0000 Subject: [PATCH 4/6] More tests, try to complete coverage --- tests/CMakeLists.txt | 4 +- tests/test_bufr_api.cc | 419 +++++++++++++++++++++++++++++++++ tests/test_bufr_descriptors.cc | 345 +++++++++++++++++++++++++++ tests/test_unit_extended.sh | 8 + 4 files changed, 775 insertions(+), 1 deletion(-) create mode 100644 tests/test_bufr_api.cc create mode 100644 tests/test_bufr_descriptors.cc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 67a8f49bc..a1a944687 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,7 +86,9 @@ list(APPEND test_c_bins test_trie test_keys_iterator test_handle_lifecycle - test_util) + test_util + test_bufr_descriptors + test_bufr_api) foreach( tool ${test_c_bins} ) diff --git a/tests/test_bufr_api.cc b/tests/test_bufr_api.cc new file mode 100644 index 000000000..12c003645 --- /dev/null +++ b/tests/test_bufr_api.cc @@ -0,0 +1,419 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for BUFR public API functions: + * - codes_bufr_handle_new_from_file + * - wmo_read_bufr_from_file / wmo_read_bufr_from_file_malloc + * - codes_bufr_keys_iterator (including rewind) + * - codes_bufr_key_is_header + * - codes_bufr_multi_element_constant_arrays_on/off + * - BUFR3 vs BUFR4 edition handling + */ + +#include "eccodes.h" +#include "grib_api_internal.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* Helper: write a BUFR sample to a temp file and return its path */ +static const char* write_bufr_sample(const char* sample_name, const char* tmpfile) +{ + int err = 0; + grib_handle* h = codes_bufr_handle_new_from_samples(NULL, sample_name); + if (!h) return NULL; + + const void* mesg = NULL; + size_t mesg_len = 0; + err = grib_get_message(h, &mesg, &mesg_len); + if (err != GRIB_SUCCESS) { + grib_handle_delete(h); + return NULL; + } + + FILE* fp = fopen(tmpfile, "wb"); + if (!fp) { + grib_handle_delete(h); + return NULL; + } + fwrite(mesg, 1, mesg_len, fp); + fclose(fp); + grib_handle_delete(h); + return tmpfile; +} + +/* ========== Test: BUFR handle from file roundtrip ========== */ +static void test_bufr_handle_from_file() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + const char* tmpfile = "temp_test_bufr_from_file.bufr"; + const char* result = write_bufr_sample("BUFR4", tmpfile); + TEST_ASSERT(result != NULL); + + /* Read back using codes_bufr_handle_new_from_file */ + FILE* fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + + grib_handle* h = codes_bufr_handle_new_from_file(NULL, fp, &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(h != NULL); + + /* Verify it's a BUFR4 message */ + long edition = 0; + err = grib_get_long(h, "edition", &edition); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition == 4); + + /* Verify product kind */ + ProductKind kind; + err = codes_get_product_kind(h, &kind); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(kind == PRODUCT_BUFR); + + /* Second read should return NULL (only one message) */ + grib_handle* h2 = codes_bufr_handle_new_from_file(NULL, fp, &err); + TEST_ASSERT(h2 == NULL); + + grib_handle_delete(h); + fclose(fp); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: wmo_read_bufr_from_file_malloc ========== */ +static void test_wmo_read_bufr_malloc() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + const char* tmpfile = "temp_test_wmo_bufr_malloc.bufr"; + const char* result = write_bufr_sample("BUFR4", tmpfile); + TEST_ASSERT(result != NULL); + + FILE* fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + + size_t size = 0; + off_t offset = 0; + void* mesg = wmo_read_bufr_from_file_malloc(fp, 0, &size, &offset, &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(mesg != NULL); + TEST_ASSERT(size > 0); + TEST_ASSERT(offset == 0); /* First message starts at offset 0 */ + + /* Verify BUFR magic number */ + unsigned char* buf = (unsigned char*)mesg; + TEST_ASSERT(buf[0] == 'B'); + TEST_ASSERT(buf[1] == 'U'); + TEST_ASSERT(buf[2] == 'F'); + TEST_ASSERT(buf[3] == 'R'); + + /* Verify 7777 terminator */ + TEST_ASSERT(buf[size - 4] == '7'); + TEST_ASSERT(buf[size - 3] == '7'); + TEST_ASSERT(buf[size - 2] == '7'); + TEST_ASSERT(buf[size - 1] == '7'); + + /* Second read should give END_OF_FILE */ + size_t size2 = 0; + off_t offset2 = 0; + void* mesg2 = wmo_read_bufr_from_file_malloc(fp, 0, &size2, &offset2, &err); + TEST_ASSERT(mesg2 == NULL); + TEST_ASSERT(err == GRIB_END_OF_FILE); + + grib_context_free(NULL, mesg); + fclose(fp); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: wmo_read_bufr_from_file (fixed buffer) ========== */ +static void test_wmo_read_bufr_fixed_buffer() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + const char* tmpfile = "temp_test_wmo_bufr_fixed.bufr"; + const char* result = write_bufr_sample("BUFR4", tmpfile); + TEST_ASSERT(result != NULL); + + FILE* fp = fopen(tmpfile, "rb"); + TEST_ASSERT(fp != NULL); + + /* Use a buffer large enough for the sample */ + unsigned char buffer[4096]; + size_t len = sizeof(buffer); + err = wmo_read_bufr_from_file(fp, buffer, &len); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(len > 0); + TEST_ASSERT(len <= sizeof(buffer)); + + /* Verify magic */ + TEST_ASSERT(buffer[0] == 'B'); + TEST_ASSERT(buffer[1] == 'U'); + TEST_ASSERT(buffer[2] == 'F'); + TEST_ASSERT(buffer[3] == 'R'); + + fclose(fp); + remove(tmpfile); + TEST_PASS(); +} + +/* ========== Test: BUFR keys iterator with rewind ========== */ +static void test_bufr_keys_iterator_rewind() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(h != NULL); + + bufr_keys_iterator* kiter = codes_bufr_keys_iterator_new(h, 0); + TEST_ASSERT(kiter != NULL); + + /* First pass: count keys */ + int count1 = 0; + while (codes_bufr_keys_iterator_next(kiter)) { + char* name = codes_bufr_keys_iterator_get_name(kiter); + TEST_ASSERT(name != NULL); + TEST_ASSERT(strlen(name) > 0); + count1++; + } + TEST_ASSERT(count1 > 0); + printf(" BUFR4 header keys (first pass): %d\n", count1); + + /* Rewind */ + int err = codes_bufr_keys_iterator_rewind(kiter); + TEST_ASSERT(err == GRIB_SUCCESS); + + /* Second pass: should get same count */ + int count2 = 0; + while (codes_bufr_keys_iterator_next(kiter)) { + count2++; + } + TEST_ASSERT(count1 == count2); + + codes_bufr_keys_iterator_delete(kiter); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: BUFR keys iterator finds expected header keys ========== */ +static void test_bufr_keys_iterator_header_keys() +{ + printf("Running %s ...\n", __func__); + + grib_handle* h = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(h != NULL); + + bufr_keys_iterator* kiter = codes_bufr_keys_iterator_new(h, 0); + TEST_ASSERT(kiter != NULL); + + int found_edition = 0; + int found_masterTableNumber = 0; + int found_bufrHeaderCentre = 0; + + while (codes_bufr_keys_iterator_next(kiter)) { + char* name = codes_bufr_keys_iterator_get_name(kiter); + if (name) { + if (strcmp(name, "edition") == 0) found_edition = 1; + if (strcmp(name, "masterTableNumber") == 0) found_masterTableNumber = 1; + if (strcmp(name, "bufrHeaderCentre") == 0) found_bufrHeaderCentre = 1; + } + } + + TEST_ASSERT(found_edition == 1); + TEST_ASSERT(found_masterTableNumber == 1); + TEST_ASSERT(found_bufrHeaderCentre == 1); + + codes_bufr_keys_iterator_delete(kiter); + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: codes_bufr_key_is_header ========== */ +static void test_bufr_key_is_header() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(h != NULL); + + /* "edition" should be a header key */ + int is_header = codes_bufr_key_is_header(h, "edition", &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(is_header == 1); + + /* "masterTableNumber" should be a header key */ + is_header = codes_bufr_key_is_header(h, "masterTableNumber", &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(is_header == 1); + + /* "bufrHeaderCentre" should be a header key */ + is_header = codes_bufr_key_is_header(h, "bufrHeaderCentre", &err); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(is_header == 1); + + /* Non-existent key */ + is_header = codes_bufr_key_is_header(h, "nonExistentKey_xyz", &err); + TEST_ASSERT(err != GRIB_SUCCESS); + + grib_handle_delete(h); + TEST_PASS(); +} + +/* ========== Test: multi_element_constant_arrays flag ========== */ +static void test_multi_element_constant_arrays() +{ + printf("Running %s ...\n", __func__); + + grib_context* c = grib_context_get_default(); + TEST_ASSERT(c != NULL); + + /* Turn on and verify flag */ + codes_bufr_multi_element_constant_arrays_on(c); + TEST_ASSERT(c->bufr_multi_element_constant_arrays == 1); + + /* Turn off and verify flag */ + codes_bufr_multi_element_constant_arrays_off(c); + TEST_ASSERT(c->bufr_multi_element_constant_arrays == 0); + + /* Toggle again to confirm idempotency */ + codes_bufr_multi_element_constant_arrays_on(c); + codes_bufr_multi_element_constant_arrays_on(c); + TEST_ASSERT(c->bufr_multi_element_constant_arrays == 1); + + codes_bufr_multi_element_constant_arrays_off(c); + TEST_ASSERT(c->bufr_multi_element_constant_arrays == 0); + + TEST_PASS(); +} + +/* ========== Test: BUFR3 vs BUFR4 edition handling ========== */ +static void test_bufr3_vs_bufr4() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + /* BUFR3 sample */ + grib_handle* h3 = codes_bufr_handle_new_from_samples(NULL, "BUFR3"); + TEST_ASSERT(h3 != NULL); + long edition3 = 0; + err = grib_get_long(h3, "edition", &edition3); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition3 == 3); + + /* BUFR4 sample */ + grib_handle* h4 = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(h4 != NULL); + long edition4 = 0; + err = grib_get_long(h4, "edition", &edition4); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition4 == 4); + + /* Both should be PRODUCT_BUFR */ + ProductKind kind3, kind4; + err = codes_get_product_kind(h3, &kind3); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(kind3 == PRODUCT_BUFR); + + err = codes_get_product_kind(h4, &kind4); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(kind4 == PRODUCT_BUFR); + + /* Message sizes should differ (BUFR4 has different section 1 format) */ + const void* mesg3 = NULL; + size_t len3 = 0; + grib_get_message(h3, &mesg3, &len3); + + const void* mesg4 = NULL; + size_t len4 = 0; + grib_get_message(h4, &mesg4, &len4); + + printf(" BUFR3 size: %zu, BUFR4 size: %zu\n", len3, len4); + TEST_ASSERT(len3 > 0); + TEST_ASSERT(len4 > 0); + + grib_handle_delete(h3); + grib_handle_delete(h4); + TEST_PASS(); +} + +/* ========== Test: BUFR local satellite sample ========== */ +static void test_bufr_local_satellite_sample() +{ + printf("Running %s ...\n", __func__); + int err = 0; + + grib_handle* h = codes_bufr_handle_new_from_samples(NULL, "BUFR4_local_satellite"); + TEST_ASSERT(h != NULL); + + long edition = 0; + err = grib_get_long(h, "edition", &edition); + TEST_ASSERT(err == GRIB_SUCCESS); + TEST_ASSERT(edition == 4); + + /* Should have a larger message than the basic BUFR4 sample */ + const void* mesg = NULL; + size_t len = 0; + grib_get_message(h, &mesg, &len); + + grib_handle* h_basic = codes_bufr_handle_new_from_samples(NULL, "BUFR4"); + TEST_ASSERT(h_basic != NULL); + const void* mesg_basic = NULL; + size_t len_basic = 0; + grib_get_message(h_basic, &mesg_basic, &len_basic); + + printf(" BUFR4_local_satellite size: %zu, BUFR4 basic size: %zu\n", len, len_basic); + TEST_ASSERT(len >= len_basic); + + grib_handle_delete(h_basic); + grib_handle_delete(h); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running BUFR API unit tests...\n\n"); + + test_bufr_handle_from_file(); + test_wmo_read_bufr_malloc(); + test_wmo_read_bufr_fixed_buffer(); + test_bufr_keys_iterator_rewind(); + test_bufr_keys_iterator_header_keys(); + test_bufr_key_is_header(); + test_multi_element_constant_arrays(); + test_bufr3_vs_bufr4(); + test_bufr_local_satellite_sample(); + + printf("\n========================================\n"); + printf("BUFR API tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_bufr_descriptors.cc b/tests/test_bufr_descriptors.cc new file mode 100644 index 000000000..67da425e8 --- /dev/null +++ b/tests/test_bufr_descriptors.cc @@ -0,0 +1,345 @@ +/* + * (C) Copyright 2005- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * + * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by + * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. + */ + +/* + * Unit tests for BUFR descriptor and descriptor array data structures: + * - grib_bufr_descriptors_array (new / push / get / pop_front / used_size / delete) + * - grib_bufr_descriptor (clone / delete / set_scale / can_be_missing) + */ + +#include "eccodes.h" +#include "grib_api_internal.h" +#include "grib_scaling.h" + +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ + } while (0) + +#define TEST_PASS() tests_passed++ + +/* Helper: manually create a bufr_descriptor without needing a table accessor */ +static bufr_descriptor* make_test_descriptor(int code, const char* shortName, + const char* units, long scale, + double reference, long width, int type) +{ + grib_context* c = grib_context_get_default(); + bufr_descriptor* d = (bufr_descriptor*)grib_context_malloc_clear(c, sizeof(bufr_descriptor)); + if (!d) return NULL; + d->context = c; + d->code = code; + d->F = code / 100000; + d->X = (code - d->F * 100000) / 1000; + d->Y = (code - d->F * 100000) % 1000; + strncpy(d->shortName, shortName, sizeof(d->shortName) - 1); + strncpy(d->units, units, sizeof(d->units) - 1); + d->scale = scale; + d->factor = codes_power(-scale, 10); + d->reference = reference; + d->width = width; + d->type = type; + d->nokey = 0; + return d; +} + +/* ========== Test: descriptors_array basic lifecycle ========== */ +static void test_descriptors_array_lifecycle() +{ + printf("Running %s ...\n", __func__); + + bufr_descriptors_array* a = grib_bufr_descriptors_array_new(10, 20); + TEST_ASSERT(a != NULL); + TEST_ASSERT(grib_bufr_descriptors_array_used_size(a) == 0); + + /* Push 3 descriptors */ + bufr_descriptor* d1 = make_test_descriptor(12101, "airTemperature", "K", 2, 0, 16, BUFR_DESCRIPTOR_TYPE_DOUBLE); + bufr_descriptor* d2 = make_test_descriptor(11001, "windDirection", "deg", 0, 0, 9, BUFR_DESCRIPTOR_TYPE_LONG); + bufr_descriptor* d3 = make_test_descriptor(11002, "windSpeed", "m/s", 1, 0, 12, BUFR_DESCRIPTOR_TYPE_DOUBLE); + TEST_ASSERT(d1 != NULL); + TEST_ASSERT(d2 != NULL); + TEST_ASSERT(d3 != NULL); + + a = grib_bufr_descriptors_array_push(a, d1); + a = grib_bufr_descriptors_array_push(a, d2); + a = grib_bufr_descriptors_array_push(a, d3); + TEST_ASSERT(grib_bufr_descriptors_array_used_size(a) == 3); + + /* Get and verify */ + bufr_descriptor* got = grib_bufr_descriptors_array_get(a, 0); + TEST_ASSERT(got == d1); + TEST_ASSERT(got->code == 12101); + + got = grib_bufr_descriptors_array_get(a, 1); + TEST_ASSERT(got == d2); + TEST_ASSERT(got->code == 11001); + + got = grib_bufr_descriptors_array_get(a, 2); + TEST_ASSERT(got == d3); + TEST_ASSERT(got->code == 11002); + + grib_bufr_descriptors_array_delete(a); + TEST_PASS(); +} + +/* ========== Test: pop_front removes from front ========== */ +static void test_descriptors_array_pop_front() +{ + printf("Running %s ...\n", __func__); + + bufr_descriptors_array* a = grib_bufr_descriptors_array_new(10, 20); + TEST_ASSERT(a != NULL); + + bufr_descriptor* d1 = make_test_descriptor(1001, "wmoBlockNumber", "Numeric", 0, 0, 7, BUFR_DESCRIPTOR_TYPE_LONG); + bufr_descriptor* d2 = make_test_descriptor(1002, "wmoStationNumber", "Numeric", 0, 0, 10, BUFR_DESCRIPTOR_TYPE_LONG); + bufr_descriptor* d3 = make_test_descriptor(1003, "wmoRegionSubArea", "Numeric", 0, 0, 3, BUFR_DESCRIPTOR_TYPE_LONG); + TEST_ASSERT(d1 != NULL && d2 != NULL && d3 != NULL); + + a = grib_bufr_descriptors_array_push(a, d1); + a = grib_bufr_descriptors_array_push(a, d2); + a = grib_bufr_descriptors_array_push(a, d3); + TEST_ASSERT(grib_bufr_descriptors_array_used_size(a) == 3); + + /* Pop front should return d1 */ + bufr_descriptor* popped = grib_bufr_descriptors_array_pop_front(a); + TEST_ASSERT(popped == d1); + TEST_ASSERT(popped->code == 1001); + TEST_ASSERT(grib_bufr_descriptors_array_used_size(a) == 2); + + /* Next element should now be d2 */ + bufr_descriptor* front = grib_bufr_descriptors_array_get(a, 0); + TEST_ASSERT(front == d2); + + /* Pop front again */ + popped = grib_bufr_descriptors_array_pop_front(a); + TEST_ASSERT(popped == d2); + TEST_ASSERT(grib_bufr_descriptors_array_used_size(a) == 1); + + /* Free the popped descriptors manually (they were removed from the array) */ + grib_bufr_descriptor_delete(d1); + grib_bufr_descriptor_delete(d2); + + grib_bufr_descriptors_array_delete(a); + TEST_PASS(); +} + +/* ========== Test: array growth beyond initial capacity ========== */ +static void test_descriptors_array_growth() +{ + printf("Running %s ...\n", __func__); + + /* Start with a small initial size */ + bufr_descriptors_array* a = grib_bufr_descriptors_array_new(5, 10); + TEST_ASSERT(a != NULL); + + const int N = 50; + for (int i = 0; i < N; i++) { + bufr_descriptor* d = make_test_descriptor(i * 1000 + 1, "test", "Numeric", 0, 0, 8, BUFR_DESCRIPTOR_TYPE_LONG); + TEST_ASSERT(d != NULL); + a = grib_bufr_descriptors_array_push(a, d); + TEST_ASSERT(a != NULL); + } + TEST_ASSERT(grib_bufr_descriptors_array_used_size(a) == (size_t)N); + + /* Verify all elements */ + for (int i = 0; i < N; i++) { + bufr_descriptor* got = grib_bufr_descriptors_array_get(a, i); + TEST_ASSERT(got != NULL); + TEST_ASSERT(got->code == i * 1000 + 1); + } + + grib_bufr_descriptors_array_delete(a); + TEST_PASS(); +} + +/* ========== Test: descriptor clone produces independent copy ========== */ +static void test_descriptor_clone() +{ + printf("Running %s ...\n", __func__); + + bufr_descriptor* orig = make_test_descriptor(12101, "airTemperature", "K", 2, 0.0, 16, BUFR_DESCRIPTOR_TYPE_DOUBLE); + TEST_ASSERT(orig != NULL); + + bufr_descriptor* clone = grib_bufr_descriptor_clone(orig); + TEST_ASSERT(clone != NULL); + TEST_ASSERT(clone != orig); /* Different pointers */ + + /* Values should match */ + TEST_ASSERT(clone->code == orig->code); + TEST_ASSERT(clone->F == orig->F); + TEST_ASSERT(clone->X == orig->X); + TEST_ASSERT(clone->Y == orig->Y); + TEST_ASSERT(clone->scale == orig->scale); + TEST_ASSERT(clone->width == orig->width); + TEST_ASSERT(clone->reference == orig->reference); + TEST_ASSERT(clone->type == orig->type); + TEST_ASSERT(strcmp(clone->shortName, orig->shortName) == 0); + TEST_ASSERT(strcmp(clone->units, orig->units) == 0); + + /* Modifying clone should not affect original */ + clone->scale = 5; + TEST_ASSERT(orig->scale == 2); + + grib_bufr_descriptor_delete(clone); + grib_bufr_descriptor_delete(orig); + TEST_PASS(); +} + +/* ========== Test: clone of NULL returns NULL ========== */ +static void test_descriptor_clone_null() +{ + printf("Running %s ...\n", __func__); + + bufr_descriptor* clone = grib_bufr_descriptor_clone(NULL); + TEST_ASSERT(clone == NULL); + + /* delete of NULL should not crash */ + grib_bufr_descriptor_delete(NULL); + + TEST_PASS(); +} + +/* ========== Test: set_scale changes type and factor ========== */ +static void test_descriptor_set_scale() +{ + printf("Running %s ...\n", __func__); + + bufr_descriptor* d = make_test_descriptor(12101, "airTemperature", "K", 0, 0, 16, BUFR_DESCRIPTOR_TYPE_LONG); + TEST_ASSERT(d != NULL); + + /* Initially scale=0, type is LONG */ + TEST_ASSERT(d->scale == 0); + TEST_ASSERT(d->type == BUFR_DESCRIPTOR_TYPE_LONG); + + /* Setting non-zero scale should change type to DOUBLE */ + grib_bufr_descriptor_set_scale(d, 3); + TEST_ASSERT(d->scale == 3); + TEST_ASSERT(d->type == BUFR_DESCRIPTOR_TYPE_DOUBLE); + /* factor should be 10^(-3) = 0.001 */ + TEST_ASSERT(fabs(d->factor - 0.001) < 1e-10); + + /* Setting scale to 0 keeps type as-is (only non-zero changes type) */ + grib_bufr_descriptor_set_scale(d, 0); + TEST_ASSERT(d->scale == 0); + /* factor should be 10^0 = 1.0 */ + TEST_ASSERT(fabs(d->factor - 1.0) < 1e-10); + + /* Negative scale => factor > 1 */ + grib_bufr_descriptor_set_scale(d, -2); + TEST_ASSERT(d->scale == -2); + TEST_ASSERT(d->type == BUFR_DESCRIPTOR_TYPE_DOUBLE); + /* factor should be 10^2 = 100.0 */ + TEST_ASSERT(fabs(d->factor - 100.0) < 1e-10); + + grib_bufr_descriptor_delete(d); + TEST_PASS(); +} + +/* ========== Test: can_be_missing logic ========== */ +static void test_descriptor_can_be_missing() +{ + printf("Running %s ...\n", __func__); + + /* Normal descriptor: can be missing */ + bufr_descriptor* d1 = make_test_descriptor(12101, "airTemperature", "K", 2, 0, 16, BUFR_DESCRIPTOR_TYPE_DOUBLE); + TEST_ASSERT(d1 != NULL); + TEST_ASSERT(grib_bufr_descriptor_can_be_missing(d1) == 1); + + /* Code 31031 (data present indicator): cannot be missing */ + bufr_descriptor* d2 = make_test_descriptor(31031, "dataPresentIndicator", "FLAG TABLE", 0, 0, 1, BUFR_DESCRIPTOR_TYPE_FLAG); + TEST_ASSERT(d2 != NULL); + TEST_ASSERT(grib_bufr_descriptor_can_be_missing(d2) == 0); + + /* Code 999999 (associated field significance): cannot be missing */ + bufr_descriptor* d3 = make_test_descriptor(999999, "associatedField", "Numeric", 0, 0, 8, BUFR_DESCRIPTOR_TYPE_LONG); + TEST_ASSERT(d3 != NULL); + TEST_ASSERT(grib_bufr_descriptor_can_be_missing(d3) == 0); + + /* Width == 1: cannot be missing */ + bufr_descriptor* d4 = make_test_descriptor(20010, "cloudCover", "%", 0, 0, 1, BUFR_DESCRIPTOR_TYPE_LONG); + TEST_ASSERT(d4 != NULL); + TEST_ASSERT(grib_bufr_descriptor_can_be_missing(d4) == 0); + + grib_bufr_descriptor_delete(d1); + grib_bufr_descriptor_delete(d2); + grib_bufr_descriptor_delete(d3); + grib_bufr_descriptor_delete(d4); + TEST_PASS(); +} + +/* ========== Test: F/X/Y decomposition ========== */ +static void test_descriptor_fxy() +{ + printf("Running %s ...\n", __func__); + + /* Element descriptor: F=0, X=12, Y=101 -> code 012101 */ + bufr_descriptor* d1 = make_test_descriptor(12101, "airTemp", "K", 0, 0, 16, BUFR_DESCRIPTOR_TYPE_DOUBLE); + TEST_ASSERT(d1 != NULL); + TEST_ASSERT(d1->F == 0); + TEST_ASSERT(d1->X == 12); + TEST_ASSERT(d1->Y == 101); + + /* Replication descriptor: F=1, X=2, Y=3 -> code 102003 */ + bufr_descriptor* d2 = make_test_descriptor(102003, "replication", "", 0, 0, 0, BUFR_DESCRIPTOR_TYPE_REPLICATION); + TEST_ASSERT(d2 != NULL); + TEST_ASSERT(d2->F == 1); + TEST_ASSERT(d2->X == 2); + TEST_ASSERT(d2->Y == 3); + + /* Operator descriptor: F=2, X=1, Y=129 -> code 201129 */ + bufr_descriptor* d3 = make_test_descriptor(201129, "operator", "", 0, 0, 0, BUFR_DESCRIPTOR_TYPE_OPERATOR); + TEST_ASSERT(d3 != NULL); + TEST_ASSERT(d3->F == 2); + TEST_ASSERT(d3->X == 1); + TEST_ASSERT(d3->Y == 129); + + /* Sequence descriptor: F=3, X=1, Y=11 -> code 301011 */ + bufr_descriptor* d4 = make_test_descriptor(301011, "sequence", "", 0, 0, 0, BUFR_DESCRIPTOR_TYPE_SEQUENCE); + TEST_ASSERT(d4 != NULL); + TEST_ASSERT(d4->F == 3); + TEST_ASSERT(d4->X == 1); + TEST_ASSERT(d4->Y == 11); + + grib_bufr_descriptor_delete(d1); + grib_bufr_descriptor_delete(d2); + grib_bufr_descriptor_delete(d3); + grib_bufr_descriptor_delete(d4); + TEST_PASS(); +} + +int main(int argc, char** argv) +{ + printf("Running BUFR descriptor unit tests...\n\n"); + + test_descriptors_array_lifecycle(); + test_descriptors_array_pop_front(); + test_descriptors_array_growth(); + test_descriptor_clone(); + test_descriptor_clone_null(); + test_descriptor_set_scale(); + test_descriptor_can_be_missing(); + test_descriptor_fxy(); + + printf("\n========================================\n"); + printf("BUFR descriptor tests: %d passed, %d failed\n", tests_passed, tests_failed); + printf("========================================\n"); + + return tests_failed > 0 ? 1 : 0; +} diff --git a/tests/test_unit_extended.sh b/tests/test_unit_extended.sh index 685b9d5d4..e10289ee9 100755 --- a/tests/test_unit_extended.sh +++ b/tests/test_unit_extended.sh @@ -56,4 +56,12 @@ echo "Running test_util..." $EXEC ${test_dir}/test_util echo "" +echo "Running test_bufr_descriptors..." +$EXEC ${test_dir}/test_bufr_descriptors +echo "" + +echo "Running test_bufr_api..." +$EXEC ${test_dir}/test_bufr_api +echo "" + echo "All new unit tests passed." From fe94d076760c2aa559e5424a9595d7a43ec7149f Mon Sep 17 00:00:00 2001 From: shahramn Date: Wed, 11 Mar 2026 21:04:09 +0000 Subject: [PATCH 5/6] Testing: Fix broken test --- tests/grib_packing_roundtrip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/grib_packing_roundtrip.sh b/tests/grib_packing_roundtrip.sh index 084fd6663..ced40852f 100755 --- a/tests/grib_packing_roundtrip.sh +++ b/tests/grib_packing_roundtrip.sh @@ -34,7 +34,7 @@ set Nj = 18; set numberOfDataPoints = 648; set numberOfValues = 648; # Set values to a ramp from 200 to 320 (temperature range in K) -set values = {$(seq 200 0.1855 320 | head -648 | tr '\n' ',' | sed 's/,$//')}; +set values = {$(seq 200 0.1855 320.1 | head -648 | tr '\n' ',' | sed 's/,$//')}; write; EOF From e73d8546c28e4dfc48dc2078dcb51b99f77c82b5 Mon Sep 17 00:00:00 2001 From: shahramn Date: Thu, 12 Mar 2026 14:54:37 +0000 Subject: [PATCH 6/6] GitHub CI: Ignore paths revised --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c76f34082..b99721091 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ on: # Trigger the workflow on pull request pull_request: paths-ignore: - - "tests/**" - "data/**" - "deprecated/**"