Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions TempFile.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// Created by James Gallagher on 1/4/26.
//

#ifndef LIBDAP_TEMPFILE_H
#define LIBDAP_TEMPFILE_H

#include <fstream>
#include <stdexcept>
#include <string>
#include <unistd.h> // mkstemp, close, unlink
#include <utility> // std::exchange
#include <vector>

namespace libdap {

class TempFile {
public:
// pattern must contain at least 6 trailing 'X' characters, e.g. "/tmp/myapp-XXXXXX"
explicit TempFile(std::string pattern,
std::ios::openmode mode = std::ios::out | std::ios::binary | std::ios::trunc) {
create_and_open(std::move(pattern), mode);
}

~TempFile() noexcept { cleanup(); }

TempFile(const TempFile &) = delete;
TempFile &operator=(const TempFile &) = delete;

TempFile(TempFile &&other) noexcept
: path_(std::move(other.path_)), stream_(std::move(other.stream_)),
unlink_on_destroy_(std::exchange(other.unlink_on_destroy_, false)) {
other.path_.clear();
}

TempFile &operator=(TempFile &&other) noexcept {
if (this != &other) {
cleanup();
path_ = std::move(other.path_);
stream_ = std::move(other.stream_);
unlink_on_destroy_ = std::exchange(other.unlink_on_destroy_, false);
other.path_.clear();
}
return *this;
}

const std::string &path() const noexcept { return path_; }

std::ofstream &stream() noexcept { return stream_; }
const std::ofstream &stream() const noexcept { return stream_; }

bool is_open() const noexcept { return stream_.is_open(); }
void flush() noexcept { stream_.flush(); }

// Optional: close early (file will still be unlinked in destructor unless you call release()).
void close_stream() {
if (stream_.is_open())
stream_.close();
}

// Optional: keep the file on disk after this object dies.
// Closes the stream and disables unlink-on-destroy, returning the path.
std::string release() noexcept {
close_stream();
unlink_on_destroy_ = false;
return path_;
}

private:
void create_and_open(std::string pattern, std::ios::openmode mode) {
// mkstemp mutates its template buffer.
std::vector<char> buf(pattern.begin(), pattern.end());
buf.push_back('\0');

int fd = ::mkstemp(buf.data());
if (fd == -1) {
throw std::runtime_error("mkstemp failed");
}

std::string created_path = buf.data();

// We can't portably attach std::ofstream to an existing fd, so close it
// and reopen by path using iostreams.
if (::close(fd) != 0) {
::unlink(created_path.c_str());
throw std::runtime_error("close(mkstemp fd) failed");
}

std::ofstream ofs;
ofs.open(created_path, mode);
if (!ofs.is_open() || !ofs) {
::unlink(created_path.c_str());
throw std::runtime_error("ofstream open failed");
}

// Commit only after everything succeeded (strong exception safety).
path_ = std::move(created_path);
stream_ = std::move(ofs);
unlink_on_destroy_ = true;
}

void cleanup() noexcept {
// Never throw from cleanup/destructor.
try {
if (stream_.is_open()) {
stream_.close();
}
} catch (...) {
// swallow
}

if (unlink_on_destroy_ && !path_.empty()) {
::unlink(path_.c_str()); // ignore errors
}

// Reset to a benign state.
unlink_on_destroy_ = false;
path_.clear();
// stream_ is already closed (or never opened); leaving it is fine.
}

std::string path_;
std::ofstream stream_;
bool unlink_on_destroy_ = false;
};

} // namespace libdap

#endif // LIBDAP_TEMPFILE_H
48 changes: 40 additions & 8 deletions tests/cmake/das-tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
file(GLOB DAS_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/das-testsuite"
"${CMAKE_CURRENT_SOURCE_DIR}/das-testsuite/*.das")

# Accumulate all per-test baseline targets into this list. Used to make the
# 'das-baselines' target that rebuilds all of the baselines.
set(_DAS_BASELINE_TARGETS "")

# Helper to register one DAS‐response test
function(add_das_test das_filename)
# # get "test.1.das" → fullname="test.1.das"
Expand All @@ -20,17 +24,45 @@ function(add_das_test das_filename)
set(baseline "${CMAKE_CURRENT_SOURCE_DIR}/das-testsuite/${das_filename}.base")
set(output "${CMAKE_CURRENT_BINARY_DIR}/${testname}.out")

# Add the CTest entry
# Put the command that runs the test in a variable so that the exact same command
# can be used with a custom target to build the test baseline.
#
# Assume das_filename has no spaces; no need to quote the variables in the
# shell command, which makes the command more readable.
set(the_test "$<TARGET_FILE:das-test> -p < ${input} > ${output} 2>&1")

# Add the CTest entry.
add_test(NAME ${testname}
COMMAND /bin/sh "-c"
# 1) run das-test, redirect all output into a temp file
# 2) diff that file against the baseline"
"\"$<TARGET_FILE:das-test>\" -p < \"${input}\" > \"${output}\" 2>&1; \
diff -b -B \"${baseline}\" \"${output}\""
COMMAND /bin/sh -c "${the_test}; diff -u -b -B ${baseline} ${output} && rm -f ${output}"
)
set_tests_properties(${testname} PROPERTIES LABELS "integration;das")

set(staged_baseline "${CMAKE_CURRENT_BINARY_DIR}/baselines/${das_filename}.base")
set(baseline_tgt "baseline-${testname}")
add_custom_target(${baseline_tgt}
COMMAND ${CMAKE_COMMAND} -E make_directory "$<SHELL_PATH:${CMAKE_CURRENT_BINARY_DIR}/baselines>"
COMMAND /bin/sh -c "${the_test}; ${CMAKE_COMMAND} -E copy ${output} ${staged_baseline} && rm -f ${output}"
BYPRODUCTS "${staged_baseline}"
COMMENT "Staging DAS baseline for ${das_filename} → ${staged_baseline}"
VERBATIM
)

# Share the target name with the parent scope so we can build an aggregate target
set(_DAS_BASELINE_TARGETS ${_DAS_BASELINE_TARGETS} ${baseline_tgt} PARENT_SCOPE)
endfunction()

foreach(dfile IN LISTS DAS_FILES)
add_das_test(${dfile})
# Register all tests (and per-test baseline targets).
# Use 'make test' or 'make check' to run these. See the top-level CMakeLists file
# for more ways to run the tests.
#
# Use 'make baseline-das_test_1' or 'cmake --build . --target baseline-das_test_1', ...
# build the baselines. Use 'ctest -R das_test_1' to run a single test.
foreach(das_file IN LISTS DAS_FILES)
add_das_test(${das_file})
endforeach()

# Aggregate target: regenerate all DAS baselines in one shot. Use
# 'cmake --build . --target das-baselines' for that.
if(_DAS_BASELINE_TARGETS)
add_custom_target(das-baselines DEPENDS ${_DAS_BASELINE_TARGETS})
endif()
38 changes: 30 additions & 8 deletions tests/cmake/dds-tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,63 @@
file(GLOB DDS_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/dds-testsuite"
"${CMAKE_CURRENT_SOURCE_DIR}/dds-testsuite/*.dds")

# Accumulate all per-test baseline targets into this list. Used to make the
# 'das-baselines' target that rebuilds all of the baselines.
set(_DDS_BASELINE_TARGETS "")

## This function will take the name of a DDS file and use it as input to a DDS
## test. There are some tricks here as well. 7/8/25 jhrg
function(add_dds_test dds_filename)
# Here the name of the dds file is morphed into something that will work
# as a cmake name (dots are not allowed in cmake names). 7/8/25 jhrg
# message(STATUS "testname: ${testname}")
get_filename_component(fullname "${dds_filename}" NAME)
# strip just ".das" → raw="test.1"
string(REGEX REPLACE "\\.dds$" "" raw "${fullname}")
# sanitize; test.1 → test_1
string(REGEX REPLACE "[^A-Za-z0-9_]" "_" testname "dds_${raw}")
# message(STATUS "testname: ${testname}")

# Paths
set(input "${CMAKE_CURRENT_SOURCE_DIR}/dds-testsuite/${dds_filename}")
set(baseline "${CMAKE_CURRENT_SOURCE_DIR}/dds-testsuite/${dds_filename}.base")
set(output "${CMAKE_CURRENT_BINARY_DIR}/${testname}.out")

set(the_test "$<TARGET_FILE:dds-test> -p < ${input} > ${output} 2>&1")

# Add the CTest entry. Here the shell is used so that we can employ redirection.
# The extra double quotes are 'best practice' for cmake, but really not needed here
# because we know that $<TARGET_FILE:dds-test> and the various variables (e.g. ${input})
# do not have spaces. The extra backslash characters make it harder to decipher
# what is going on. 7/8/25 jhrg
# ...so removed them. jhrg 12/30/25
add_test(NAME ${testname}
COMMAND /bin/sh "-c"
"\"$<TARGET_FILE:dds-test>\" -p < \"${input}\" > \"${output}\" 2>&1; \
diff -b -B \"${baseline}\" \"${output}\""
COMMAND /bin/sh -c "${the_test}; diff -u -b -B ${baseline} ${output} && rm -f ${output}"
)
# This makes it so we can run just these tests and also makes it easy to run the
# unit tests _before_ the integration tests with a 'check' target. See the top-level
# CMakeLists file. 7/8/25 jhrg
set_tests_properties(${testname} PROPERTIES LABELS "integration;dds")

set(staged_baseline "${CMAKE_CURRENT_BINARY_DIR}/baselines/${dds_filename}.base")
set(baseline_tgt "baseline-${testname}")
add_custom_target(${baseline_tgt}
COMMAND ${CMAKE_COMMAND} -E make_directory "$<SHELL_PATH:${CMAKE_CURRENT_BINARY_DIR}/baselines>"
COMMAND /bin/sh -c "${the_test}; ${CMAKE_COMMAND} -E copy ${output} ${staged_baseline} && rm -f ${output}"
BYPRODUCTS "${staged_baseline}"
COMMENT "Staging DAS baseline for ${das_filename} → ${staged_baseline}"
VERBATIM
)

# Share the target name with the parent scope so we can build an aggregate target
set(_DDS_BASELINE_TARGETS ${_DDS_BASELINE_TARGETS} ${baseline_tgt} PARENT_SCOPE)

endfunction()

## Iterate over all of the DDS filed and make a cmake/ctest for each one. 7/8/25 jhrg
foreach(dfile IN LISTS DDS_FILES)
add_dds_test(${dfile})
foreach(dds_file IN LISTS DDS_FILES)
add_dds_test(${dds_file})
endforeach()

# Aggregate target: regenerate all DDS baselines in one shot. Use
# 'cmake --build . --target dds-baselines' for that.
if(_DDS_BASELINE_TARGETS)
add_custom_target(dds-baselines DEPENDS ${_DDS_BASELINE_TARGETS})
endif()
61 changes: 40 additions & 21 deletions tests/cmake/dmr-tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function(dmr_parse_ce_test test_number test_input ce test_baseline)
# 1) run das-test, redirect all output into a temp file
# 2) diff that file against the baseline"
"\"$<TARGET_FILE:dmr-test>\" -x -p \"${input}\" -c \"${ce}\" > \"${output}\" 2>&1; \
diff -b -B \"${baseline}\" \"${output}\""
diff -b -B \"${baseline}\" \"${output}\" && rm -f \"${output}\""
)
# This makes it so we can run just these tests and also makes it easy to run the
# unit tests _before_ the integration tests with a 'check' target. See the top-leve
Expand Down Expand Up @@ -107,7 +107,7 @@ function(dmr_intern_test number input baseline)
add_test(NAME ${testname}
COMMAND /bin/sh "-c"
"\"$<TARGET_FILE:dmr-test>\" -x -i \"${input}\" > \"${output}\" 2>&1; \
diff -b -B \"${baseline}\" \"${output}\""
diff -b -B \"${baseline}\" \"${output}\" && rm -f \"${output}\""
)

set_tests_properties(${testname} PROPERTIES LABELS "integration;dmr;dmr-intern")
Expand Down Expand Up @@ -171,7 +171,7 @@ function(dmr_trans_test number input ce func baseline byte_order)
sed 's@<Value>[0-9a-f][0-9a-f]*</Value>@${checksum_replacement}@' \"${output}\" > \"${output}_univ\"; \
mv \"${output}_univ\" \"${output}\"; \
fi; \
diff -b -B \"${baseline}\" \"${output}\";"
diff -b -B \"${baseline}\" \"${output}\" && rm -f \"${output}\""
)

set_tests_properties(${testname} PROPERTIES LABELS "integration;dmr;trans")
Expand All @@ -181,7 +181,8 @@ function(dmr_trans_test number input ce func baseline byte_order)
# files with the same (i.e., conflicting) names. I tied using the
# RESOURCE_LOCKS property, but it was slower and did not stop the collisions.
# 7/12/25 jhrg
set_tests_properties(${testname} PROPERTIES RUN_SERIAL TRUE)
# set_tests_properties(${testname} PROPERTIES RUN_SERIAL TRUE)
# dmr-test now uses multi-process safe temp files. jhrg 1/5/26
endfunction()

# DMR translation tests → dmr_trans_test(number, input, ce, func, baseline, byte_order)
Expand Down Expand Up @@ -384,23 +385,41 @@ dmr_trans_test(146 vol_1_ce_10.xml "lat[10:11][10:11];lon[10:11][10:11]" "scale(
## of word order and they use CEs that have operators (!, <=, ...). Making a test name substituting
## those chars with '_' doesn't make unique test names. But, for this we can use the baseline
## names. Also, some of these tests are expected to fail. 7/14/25 jhrg

# New version: We switched to test numbers a while back to avoid the hack of making names
# from the various arguments, and I updated the code so include baseline generation. For
# the latter, I switched to an external script to run the C++ CLI program 'dmr-test.'
# This adds a layer of indirection, but it makes the text passed to cmake's add_test()
# easier to understand/debug. jhrg 1/2/26
#
# Notes about this new version:
#
# Run all the tests:
# ctest -V
# Update all the baselines (see the script 'run_dmr_series_tes.sh' for how this works).
# UPDATE_BASELINES=1 ctest -V
# Update just one baseline:
# UPDATE_BASELINES=1 ctest -R dmr_series_test_147 -V

function(dmr_series_test number input ce baseline xfail)
set(testname "dmr_series_test_${number}")

set(input "${CMAKE_CURRENT_SOURCE_DIR}/dmr-testsuite/${input}")
set(baseline "${CMAKE_CURRENT_SOURCE_DIR}/dmr-testsuite/universal/${baseline}")
set(output "${CMAKE_CURRENT_BINARY_DIR}/${testname}.out")
set(input_path "${CMAKE_CURRENT_SOURCE_DIR}/dmr-testsuite/${input}")
set(baseline_path "${CMAKE_CURRENT_SOURCE_DIR}/dmr-testsuite/universal/${baseline}")
set(output_path "${CMAKE_CURRENT_BINARY_DIR}/${testname}.out")
set(runner "${CMAKE_CURRENT_SOURCE_DIR}/dmr-testsuite/run_dmr_series_test.sh")

add_test(NAME ${testname}
COMMAND /bin/sh "-c"
"\"$<TARGET_FILE:dmr-test>\" -C -x -e -t \"${input}\" -c \"${ce}\" > \"${output}\" 2>&1; \
sed 's@<Value>[0-9a-f][0-9a-f]*</Value>@@' \"${output}\" > \"${output}_univ\"; \
mv \"${output}_univ\" \"${output}\"; \
diff -b -B \"${baseline}\" \"${output}\""
COMMAND bash "${runner}"
"$<TARGET_FILE:dmr-test>"
"${input_path}"
"${ce}"
"${baseline_path}"
"${output_path}"
)

set_tests_properties(${testname} PROPERTIES LABELS "integration;dmr;dmr-series")
set_tests_properties(${testname} PROPERTIES RUN_SERIAL TRUE)

if("${xfail}" STREQUAL "xfail")
set_tests_properties(${testname} PROPERTIES WILL_FAIL TRUE)
endif()
Expand All @@ -423,18 +442,18 @@ dmr_series_test(156 test_simple_7.xml "s|1024<=i1<=32768" test_simple_7.xml.f9.t
dmr_series_test(157 test_simple_7.xml "s|i1>=1024.0" test_simple_7.xml.fa.trans_base "pass")

## \\\" --> \\ is a literal slash and \" is a literal double quote. 7/14/25 jhrg
dmr_series_test(158 test_simple_7.xml "s|s==\\\"Silly test string: 2\\\"" test_simple_7.xml.fs1.trans_base "pass")
dmr_series_test(159 test_simple_7.xml "s|s!=\\\"Silly test string: 2\\\"" test_simple_7.xml.fs2.trans_base "pass")
dmr_series_test(160 test_simple_7.xml "s|s<\\\"Silly test string: 2\\\"" test_simple_7.xml.fs3.trans_base "pass")
dmr_series_test(161 test_simple_7.xml "s|s<=\\\"Silly test string: 2\\\"" test_simple_7.xml.fs4.trans_base "pass")
dmr_series_test(162 test_simple_7.xml "s|s>\\\"Silly test string: 2\\\"" test_simple_7.xml.fs5.trans_base "pass")
dmr_series_test(163 test_simple_7.xml "s|s>=\\\"Silly test string: 2\\\"" test_simple_7.xml.fs6.trans_base "pass")
dmr_series_test(164 test_simple_7.xml "s|s~=\\\".*2\\\"" test_simple_7.xml.fs7.trans_base "pass")
dmr_series_test(158 test_simple_7.xml "s|s==\"Silly test string: 2\"" test_simple_7.xml.fs1.trans_base "pass")
dmr_series_test(159 test_simple_7.xml "s|s!=\"Silly test string: 2\"" test_simple_7.xml.fs2.trans_base "pass")
dmr_series_test(160 test_simple_7.xml "s|s<\"Silly test string: 2\"" test_simple_7.xml.fs3.trans_base "pass")
dmr_series_test(161 test_simple_7.xml "s|s<=\"Silly test string: 2\"" test_simple_7.xml.fs4.trans_base "pass")
dmr_series_test(162 test_simple_7.xml "s|s>\"Silly test string: 2\"" test_simple_7.xml.fs5.trans_base "pass")
dmr_series_test(163 test_simple_7.xml "s|s>=\"Silly test string: 2\"" test_simple_7.xml.fs6.trans_base "pass")
dmr_series_test(164 test_simple_7.xml "s|s~=\".*2\"" test_simple_7.xml.fs7.trans_base "pass")

# Test filtering a sequence that has only one field projected, including filtering on the values
# of a filed not projected.
dmr_series_test(165 test_simple_7.xml "s{i1}|i1<32768" test_simple_7.xml.g1.trans_base "pass")
dmr_series_test(166 test_simple_7.xml "s{i1}|s<=\\\"Silly test string: 2\\\"" test_simple_7.xml.g1.trans_base "pass")
dmr_series_test(166 test_simple_7.xml "s{i1}|s<=\"Silly test string: 2\"" test_simple_7.xml.g1.trans_base "pass")

# A nested sequence with floats in the outer sequence and the int, string combination in the inner
dmr_series_test(167 test_simple_8.1.xml "outer" test_simple_8.1.xml.f1.trans_base "pass")
Expand Down
Loading