diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 62e7186360..31b9becc0c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -26,11 +26,11 @@ __ ## I don't want to read this whole thing I just have a question! -> **Note:** Please do not file an issue to ask a general question about LAMMPS, its features, how to use specific commands, or how perform simulations or analysis in LAMMPS. Instead post your question to the ['lammps-users' mailing list](https://lammps.sandia.gov/mail.html). You do not need to be subscribed to post to the list (but a mailing list subscription avoids having your post delayed until it is approved by a mailing list moderator). Most posts to the mailing list receive a response within less than 24 hours. Before posting to the mailing list, please read the [mailing list guidelines](https://lammps.sandia.gov/guidelines.html). Following those guidelines will help greatly to get a helpful response. Always mention which LAMMPS version you are using. +> **Note:** Please do not file an issue to ask a general question about LAMMPS, its features, how to use specific commands, or how perform simulations or analysis in LAMMPS. Instead post your question to either the ['lammps-users' mailing list](https://lammps.sandia.gov/mail.html) or the [LAMMPS Material Science Discourse forum](https://matsci.org/lammps). You do not need to be subscribed to post to the list (but a mailing list subscription avoids having your post delayed until it is approved by a mailing list moderator). Most posts to the mailing list receive a response within less than 24 hours. Before posting to the mailing list, please read the [mailing list guidelines](https://lammps.sandia.gov/guidelines.html). Following those guidelines will help greatly to get a helpful response. Always mention which LAMMPS version you are using. The LAMMPS forum was recently created as part of a larger effort to build a materials science community and have discussions not just about using LAMMPS. Thus the forum may be also used for discussions that would be off-topic for the mailing list. Those will just have to be moved to a more general category. ## How Can I Contribute? -There are several ways how you can actively contribute to the LAMMPS project: you can discuss compiling and using LAMMPS, and solving LAMMPS related problems with other LAMMPS users on the lammps-users mailing list, you can report bugs or suggest enhancements by creating issues on GitHub (or posting them to the lammps-users mailing list), and you can contribute by submitting pull requests on GitHub or e-mail your code +There are several ways how you can actively contribute to the LAMMPS project: you can discuss compiling and using LAMMPS, and solving LAMMPS related problems with other LAMMPS users on the lammps-users mailing list, you can report bugs or suggest enhancements by creating issues on GitHub (or posting them to the lammps-users mailing list or posting in the LAMMPS Materials Science Discourse forum), and you can contribute by submitting pull requests on GitHub or e-mail your code to one of the [LAMMPS core developers](https://lammps.sandia.gov/authors.html). As you may see from the aforementioned developer page, the LAMMPS software package includes the efforts of a very large number of contributors beyond the principal authors and maintainers. ### Discussing How To Use LAMMPS @@ -42,6 +42,8 @@ Anyone can browse/search previous questions/answers in the archives. You do not If you post a message and you are a subscriber, your message will appear immediately. If you are not a subscriber, your message will be moderated, which typically takes one business day. Either way, when someone replies the reply will usually be sent to both, your personal email address and the mailing list. When replying to people, that responded to your post to the list, please always included the mailing list in your replies (i.e. use "Reply All" and **not** "Reply"). Responses will appear on the list in a few minutes, but it can take a few hours for postings and replies to show up in the SourceForge archive. Sending replies also to the mailing list is important, so that responses are archived and people with a similar issue can search for possible solutions in the mailing list archive. +The LAMMPS Materials Science Discourse forum was created recently to facilitate discussion not just about LAMMPS and as part of a larger effort towards building a materials science community. The forum contains a read-only sub-category with the continually updated mailing list archive, so you won't miss anything by joining only the forum and not the mailing list. + ### Reporting Bugs While developers writing code for LAMMPS are careful to test their code, LAMMPS is such a large and complex software, that it is impossible to test for all combinations of features under all normal and not so normal circumstances. Thus bugs do happen, and if you suspect, that you have encountered one, please try to document it and report it as an [Issue](https://github.com/lammps/lammps/issues) on the LAMMPS GitHub project web page. However, before reporting a bug, you need to check whether this is something that may have already been corrected. The [Latest Features and Bug Fixes in LAMMPS](https://lammps.sandia.gov/bug.html) web page lists all significant changes to LAMMPS over the years. It also tells you what the current latest development version of LAMMPS is, and you should test whether your issue still applies to that version. diff --git a/cmake/Modules/Packages/USER-INTEL.cmake b/cmake/Modules/Packages/USER-INTEL.cmake index 90ab6167a3..ecad135b70 100644 --- a/cmake/Modules/Packages/USER-INTEL.cmake +++ b/cmake/Modules/Packages/USER-INTEL.cmake @@ -79,9 +79,11 @@ if(INTEL_ARCH STREQUAL "KNL") else() if(CMAKE_CXX_COMPILER_ID STREQUAL "Intel") include(CheckCXXCompilerFlag) - foreach(_FLAG -O2 -fp-model fast=2 -no-prec-div -qoverride-limits -qopt-zmm-usage=high -qno-offload -fno-alias -ansi-alias -restrict) - check_cxx_compiler_flag("${_FLAG}" COMPILER_SUPPORTS${_FLAG}) - if(COMPILER_SUPPORTS${_FLAG}) + foreach(_FLAG -O2 "-fp-model fast=2" -no-prec-div -qoverride-limits -qopt-zmm-usage=high -qno-offload -fno-alias -ansi-alias -restrict) + string(REGEX REPLACE "[ =\"]" "" _FLAGX ${_FLAG}) + check_cxx_compiler_flag("${_FLAG}" COMPILER_SUPPORTS${_FLAGX}) + if(COMPILER_SUPPORTS${_FLAGX}) + separate_arguments(_FLAG UNIX_COMMAND "${_FLAG}") target_compile_options(lammps PRIVATE ${_FLAG}) endif() endforeach() diff --git a/cmake/Modules/generate_lmpgitversion.cmake b/cmake/Modules/generate_lmpgitversion.cmake index 4ff01c7501..b19716d74b 100644 --- a/cmake/Modules/generate_lmpgitversion.cmake +++ b/cmake/Modules/generate_lmpgitversion.cmake @@ -17,7 +17,7 @@ if(GIT_FOUND AND EXISTS ${LAMMPS_DIR}/.git) ERROR_QUIET WORKING_DIRECTORY ${LAMMPS_DIR} OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND ${GIT_EXECUTABLE} describe --dirty=-modified + execute_process(COMMAND ${GIT_EXECUTABLE} describe --dirty=-modified --always OUTPUT_VARIABLE temp_git_describe ERROR_QUIET WORKING_DIRECTORY ${LAMMPS_DIR} diff --git a/doc/src/Developer_utils.rst b/doc/src/Developer_utils.rst index 0d0a9f1791..44210218c0 100644 --- a/doc/src/Developer_utils.rst +++ b/doc/src/Developer_utils.rst @@ -9,8 +9,8 @@ reading or writing to files with error checking or translation of strings into specific types of numbers with checking for validity. This reduces redundant implementations and encourages consistent behavior. -I/O with status check -^^^^^^^^^^^^^^^^^^^^^ +I/O with status check and similar functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The the first two functions are wrappers around the corresponding C library calls ``fgets()`` or ``fread()``. They will check if there @@ -19,6 +19,14 @@ In that case, the functions will stop with an error message, indicating the name of the problematic file, if possible unless the *error* argument is a NULL pointer. +The :cpp:func:`fgets_trunc` function will work similar for ``fgets()`` +but it will read in a whole line (i.e. until the end of line or end +of file), but store only as many characters as will fit into the buffer +including a final newline character and the terminating NULL byte. +If the line in the file is longer it will thus be truncated in the buffer. +This function is used by :cpp:func:`read_lines_from_file` to read individual +lines but make certain they follow the size constraints. + The :cpp:func:`read_lines_from_file` function will read the requested number of lines of a maximum length into a buffer and will return 0 if successful or 1 if not. It also guarantees that all lines are @@ -33,6 +41,9 @@ NULL character. .. doxygenfunction:: sfread :project: progguide +.. doxygenfunction:: fgets_trunc + :project: progguide + .. doxygenfunction:: read_lines_from_file :project: progguide diff --git a/doc/src/read_data.rst b/doc/src/read_data.rst index 5d74d9c28b..bd7c480af2 100644 --- a/doc/src/read_data.rst +++ b/doc/src/read_data.rst @@ -223,6 +223,9 @@ The structure of the data file is important, though many settings and sections are optional or can come in any order. See the examples directory for sample data files for different problems. +The file will be read line by line, but there is a limit of 254 +characters per line and characters beyond that limit will be ignored. + A data file has a header and a body. The header appears first. The first line of the header is always skipped; it typically contains a description of the file. Then lines are read one at a time. Lines diff --git a/doc/src/variable.rst b/doc/src/variable.rst index af324c180f..4747c492aa 100644 --- a/doc/src/variable.rst +++ b/doc/src/variable.rst @@ -363,12 +363,14 @@ variable, as discussed below. The rules for formatting the file are as follows. Each time a set of per-atom values is read, a non-blank line is searched for in the file. -A comment character "#" can be used anywhere on a line; text starting -with the comment character is stripped. Blank lines are skipped. The -first "word" of a non-blank line, delimited by white-space, is read as -the count N of per-atom lines to immediately follow. N can be the -total number of atoms in the system, or only a subset. The next N -lines have the following format +The file is read line by line but only up to 254 characters are used. +The rest are ignored. A comment character "#" can be used anywhere +on a line and all text following and the "#" character are ignored; +text starting with the comment character is stripped. Blank lines +are skipped. The first "word" of a non-blank line, delimited by +white-space, is read as the count N of per-atom lines to immediately +follow. N can be the total number of atoms in the system, or only a +subset. The next N lines have the following format .. parsed-literal:: diff --git a/src/KOKKOS/kokkos.cpp b/src/KOKKOS/kokkos.cpp index 3153e9a752..67202612a4 100644 --- a/src/KOKKOS/kokkos.cpp +++ b/src/KOKKOS/kokkos.cpp @@ -266,7 +266,7 @@ KokkosLMP::KokkosLMP(LAMMPS *lmp, int narg, char **arg) : Pointers(lmp) #if defined(MPICH) && defined(MVAPICH2_VERSION) char* str; gpu_aware_flag = 0; - if ((str = getenv("MV2_ENABLE_CUDA"))) + if ((str = getenv("MV2_USE_CUDA"))) if ((strcmp(str,"1") == 0)) gpu_aware_flag = 1; diff --git a/src/compute_chunk_atom.cpp b/src/compute_chunk_atom.cpp index 26967a1b12..16090eb0ec 100644 --- a/src/compute_chunk_atom.cpp +++ b/src/compute_chunk_atom.cpp @@ -155,6 +155,7 @@ ComputeChunkAtom::ComputeChunkAtom(LAMMPS *lmp, int narg, char **arg) : if ((which == ArgInfo::UNKNOWN) || (which == ArgInfo::NONE) || (argi.get_dim() > 1)) error->all(FLERR,"Illegal compute chunk/atom command"); + iarg = 4; } // optional args diff --git a/src/read_data.cpp b/src/read_data.cpp index f039ba9879..babb8d08f6 100644 --- a/src/read_data.cpp +++ b/src/read_data.cpp @@ -41,24 +41,25 @@ using namespace LAMMPS_NS; -#define MAXLINE 256 -#define LB_FACTOR 1.1 -#define CHUNK 1024 -#define DELTA 4 // must be 2 or larger -#define MAXBODY 32 // max # of lines in one body +static constexpr int MAXLINE = 256; +static constexpr double LB_FACTOR = 1.1; +static constexpr int CHUNK = 1024; +static constexpr int DELTA = 4; // must be 2 or larger +static constexpr int MAXBODY = 32; // max # of lines in one body - // customize for new sections -#define NSECTIONS 25 // change when add to header::section_keywords +// customize for new sections +// change when add to header::section_keywords +static constexpr int NSECTIONS = 25; enum{NONE,APPEND,VALUE,MERGE}; // pair style suffixes to ignore // when matching Pair Coeffs comment to currently-defined pair style -const char *suffixes[] = {"/cuda","/gpu","/opt","/omp","/kk", - "/coul/cut","/coul/long","/coul/msm", - "/coul/dsf","/coul/debye","/coul/charmm", - nullptr}; +static const char *suffixes[] = {"/cuda","/gpu","/opt","/omp","/kk", + "/coul/cut","/coul/long","/coul/msm", + "/coul/dsf","/coul/debye","/coul/charmm", + nullptr}; /* ---------------------------------------------------------------------- */ @@ -304,6 +305,12 @@ void ReadData::command(int narg, char **arg) extra_dihedral_types || extra_improper_types)) error->all(FLERR,"Cannot use read_data extra with add flag"); + // check if data file is available and readable + + if (!utils::file_is_readable(arg[0])) + error->all(FLERR,fmt::format("Cannot open file {}: {}", + arg[0], utils::getsyserror())); + // first time system initialization if (addflag == NONE) { @@ -950,7 +957,7 @@ void ReadData::header(int firstpass) // skip 1st line of file if (me == 0) { - char *eof = fgets(line,MAXLINE,fp); + char *eof = utils::fgets_trunc(line,MAXLINE,fp); if (eof == nullptr) error->one(FLERR,"Unexpected end of data file"); } @@ -959,7 +966,7 @@ void ReadData::header(int firstpass) // read a line and bcast length if (me == 0) { - if (fgets(line,MAXLINE,fp) == nullptr) n = 0; + if (utils::fgets_trunc(line,MAXLINE,fp) == nullptr) n = 0; else n = strlen(line) + 1; } MPI_Bcast(&n,1,MPI_INT,0,world); @@ -1662,7 +1669,7 @@ void ReadData::bodies(int firstpass, AtomVec *ptr) m = 0; while (nchunk < nmax && nline <= CHUNK-MAXBODY) { - eof = fgets(&buffer[m],MAXLINE,fp); + eof = utils::fgets_trunc(&buffer[m],MAXLINE,fp); if (eof == nullptr) error->one(FLERR,"Unexpected end of data file"); rv = sscanf(&buffer[m],"%d %d %d",&tmp,&ninteger,&ndouble); if (rv != 3) @@ -1676,7 +1683,7 @@ void ReadData::bodies(int firstpass, AtomVec *ptr) nword = 0; while (nword < ninteger) { - eof = fgets(&buffer[m],MAXLINE,fp); + eof = utils::fgets_trunc(&buffer[m],MAXLINE,fp); if (eof == nullptr) error->one(FLERR,"Unexpected end of data file"); ncount = utils::trim_and_count_words(&buffer[m]); if (ncount == 0) @@ -1690,7 +1697,7 @@ void ReadData::bodies(int firstpass, AtomVec *ptr) nword = 0; while (nword < ndouble) { - eof = fgets(&buffer[m],MAXLINE,fp); + eof = utils::fgets_trunc(&buffer[m],MAXLINE,fp); if (eof == nullptr) error->one(FLERR,"Unexpected end of data file"); ncount = utils::trim_and_count_words(&buffer[m]); if (ncount == 0) @@ -1994,15 +2001,15 @@ void ReadData::parse_keyword(int first) if (me == 0) { if (!first) { - if (fgets(line,MAXLINE,fp) == nullptr) eof = 1; + if (utils::fgets_trunc(line,MAXLINE,fp) == nullptr) eof = 1; } while (eof == 0 && done == 0) { int blank = strspn(line," \t\n\r"); if ((blank == (int)strlen(line)) || (line[blank] == '#')) { - if (fgets(line,MAXLINE,fp) == nullptr) eof = 1; + if (utils::fgets_trunc(line,MAXLINE,fp) == nullptr) eof = 1; } else done = 1; } - if (fgets(buffer,MAXLINE,fp) == nullptr) { + if (utils::fgets_trunc(buffer,MAXLINE,fp) == nullptr) { eof = 1; buffer[0] = '\0'; } @@ -2056,7 +2063,7 @@ void ReadData::skip_lines(bigint n) if (me) return; if (n <= 0) return; char *eof = nullptr; - for (bigint i = 0; i < n; i++) eof = fgets(line,MAXLINE,fp); + for (bigint i = 0; i < n; i++) eof = utils::fgets_trunc(line,MAXLINE,fp); if (eof == nullptr) error->one(FLERR,"Unexpected end of data file"); } diff --git a/src/text_file_reader.cpp b/src/text_file_reader.cpp index b0d5bef53e..291d4348ea 100644 --- a/src/text_file_reader.cpp +++ b/src/text_file_reader.cpp @@ -41,7 +41,7 @@ using namespace LAMMPS_NS; * \param filetype Description of file type for error messages */ TextFileReader::TextFileReader(const std::string &filename, const std::string &filetype) - : filename(filename), filetype(filetype), ignore_comments(true) + : filetype(filetype), closefp(true), ignore_comments(true) { fp = fopen(filename.c_str(), "r"); @@ -51,10 +51,33 @@ TextFileReader::TextFileReader(const std::string &filename, const std::string &f } } +/** + * \overload + * +\verbatim embed:rst + +This function is useful in combination with :cpp:func:`utils::open_potential`. + +.. note:: + + The FILE pointer is not closed in the destructor, but will be advanced + when reading from it. + +\endverbatim + * + * \param fp File descriptor of the already opened file + * \param filetype Description of file type for error messages */ + +TextFileReader::TextFileReader(FILE *fp, const std::string &filetype) + : filetype(filetype), closefp(false), fp(fp), ignore_comments(true) +{ + if (fp == nullptr) throw FileReaderException("Invalid file descriptor"); +} + /** Closes the file */ TextFileReader::~TextFileReader() { - fclose(fp); + if (closefp) fclose(fp); } /** Read the next line and ignore it */ @@ -163,5 +186,8 @@ void TextFileReader::next_dvector(double * list, int n) { * \return ValueTokenizer object for read in text */ ValueTokenizer TextFileReader::next_values(int nparams, const std::string &separators) { - return ValueTokenizer(next_line(nparams), separators); + char *ptr = next_line(nparams); + if (ptr == nullptr) + throw EOFException(fmt::format("Missing line in {} file!", filetype)); + return ValueTokenizer(line, separators); } diff --git a/src/text_file_reader.h b/src/text_file_reader.h index 1b7ca73fed..327d57c059 100644 --- a/src/text_file_reader.h +++ b/src/text_file_reader.h @@ -25,8 +25,8 @@ namespace LAMMPS_NS { class TextFileReader { - std::string filename; std::string filetype; + bool closefp; static constexpr int MAXLINE = 1024; char line[MAXLINE]; FILE *fp; @@ -35,6 +35,8 @@ namespace LAMMPS_NS bool ignore_comments; //!< Controls whether comments are ignored TextFileReader(const std::string &filename, const std::string &filetype); + TextFileReader(FILE *fp, const std::string &filetype); + ~TextFileReader(); void skip_line(); diff --git a/src/tokenizer.cpp b/src/tokenizer.cpp index ea8ff2ce43..38cdfa73fb 100644 --- a/src/tokenizer.cpp +++ b/src/tokenizer.cpp @@ -68,6 +68,28 @@ Tokenizer::Tokenizer(Tokenizer && rhs) : reset(); } +Tokenizer& Tokenizer::operator=(const Tokenizer& other) +{ + Tokenizer tmp(other); + swap(tmp); + return *this; +} + +Tokenizer& Tokenizer::operator=(Tokenizer&& other) +{ + Tokenizer tmp(std::move(other)); + swap(tmp); + return *this; +} + +void Tokenizer::swap(Tokenizer& other) +{ + std::swap(text, other.text); + std::swap(separators, other.separators); + std::swap(start, other.start); + std::swap(ntokens, other.ntokens); +} + /*! Re-position the tokenizer state to the first word, * i.e. the first non-separator character */ void Tokenizer::reset() { @@ -181,6 +203,25 @@ ValueTokenizer::ValueTokenizer(const ValueTokenizer &rhs) : tokens(rhs.tokens) { ValueTokenizer::ValueTokenizer(ValueTokenizer &&rhs) : tokens(std::move(rhs.tokens)) { } +ValueTokenizer& ValueTokenizer::operator=(const ValueTokenizer& other) +{ + ValueTokenizer tmp(other); + swap(tmp); + return *this; +} + +ValueTokenizer& ValueTokenizer::operator=(ValueTokenizer&& other) +{ + ValueTokenizer tmp(std::move(other)); + swap(tmp); + return *this; +} + +void ValueTokenizer::swap(ValueTokenizer& other) +{ + std::swap(tokens, other.tokens); +} + /*! Indicate whether more tokens are available * * \return true if there are more tokens, false if not */ diff --git a/src/tokenizer.h b/src/tokenizer.h index a17ab13d04..8cc6d2d42b 100644 --- a/src/tokenizer.h +++ b/src/tokenizer.h @@ -37,8 +37,9 @@ public: Tokenizer(const std::string &str, const std::string &separators = TOKENIZER_DEFAULT_SEPARATORS); Tokenizer(Tokenizer &&); Tokenizer(const Tokenizer &); - Tokenizer& operator=(const Tokenizer&) = default; - Tokenizer& operator=(Tokenizer&&) = default; + Tokenizer& operator=(const Tokenizer&); + Tokenizer& operator=(Tokenizer&&); + void swap(Tokenizer &); void reset(); void skip(int n=1); @@ -93,8 +94,9 @@ public: ValueTokenizer(const std::string &str, const std::string &separators = TOKENIZER_DEFAULT_SEPARATORS); ValueTokenizer(const ValueTokenizer &); ValueTokenizer(ValueTokenizer &&); - ValueTokenizer& operator=(const ValueTokenizer&) = default; - ValueTokenizer& operator=(ValueTokenizer&&) = default; + ValueTokenizer& operator=(const ValueTokenizer&); + ValueTokenizer& operator=(ValueTokenizer&&); + void swap(ValueTokenizer &); std::string next_string(); tagint next_tagint(); diff --git a/src/utils.cpp b/src/utils.cpp index c501e89557..edb9e0e4da 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -170,6 +170,42 @@ const char *utils::guesspath(char *buf, int len, FILE *fp) return buf; } +// read line into buffer. if line is too long keep reading until EOL or EOF +// but return only the first part with a newline at the end. + +char *utils::fgets_trunc(char *buf, int size, FILE *fp) +{ + constexpr int MAXDUMMY = 256; + char dummy[MAXDUMMY]; + char *ptr = fgets(buf,size,fp); + + // EOF + if (!ptr) return nullptr; + + int n = strlen(buf); + + // line is shorter than buffer, append newline if needed, + if (n < size-2) { + if (buf[n-1] != '\n') { + buf[n] = '\n'; + buf[n+1] = '\0'; + } + return buf; + + // line fits exactly. overwrite last but one character. + } else buf[size-2] = '\n'; + + // continue reading into dummy buffer until end of line or file + do { + ptr = fgets(dummy,MAXDUMMY,fp); + if (ptr) n = strlen(ptr); + else n = 0; + } while (n == MAXDUMMY-1 && ptr[MAXDUMMY-1] != '\n'); + + // return first chunk + return buf; +} + #define MAXPATHLENBUF 1024 /* like fgets() but aborts with an error or EOF is encountered */ void utils::sfgets(const char *srcname, int srcline, char *s, int size, @@ -240,13 +276,12 @@ int utils::read_lines_from_file(FILE *fp, int nlines, int nmax, if (me == 0) { if (fp) { for (int i = 0; i < nlines; i++) { - ptr = fgets(ptr,nmax,fp); + ptr = fgets_trunc(ptr,nmax,fp); if (!ptr) break; // EOF? - // advance ptr to end of string and append newline char if needed. + // advance ptr to end of string ptr += strlen(ptr); - if (*(--ptr) != '\n') *(++ptr) = '\n'; // ensure buffer is null terminated. null char is start of next line. - *(++ptr) = '\0'; + *ptr = '\0'; } } } diff --git a/src/utils.h b/src/utils.h index d0ac15adbb..d60ba0a4f6 100644 --- a/src/utils.h +++ b/src/utils.h @@ -86,6 +86,20 @@ namespace LAMMPS_NS { std::string getsyserror(); + /** Wrapper around fgets() which reads whole lines but truncates the + * data to the buffer size and ensures a newline char at the end. + * + * This function is useful for reading line based text files with + * possible comments that should be parsed later. This applies to + * data files, potential files, atomfile variable files and so on. + * It is used instead of fgets() by utils::read_lines_from_file(). + * + * \param s buffer for storing the result of fgets() + * \param size size of buffer s (max number of bytes returned) + * \param fp file pointer used by fgets() */ + + char *fgets_trunc(char *s, int size, FILE *fp); + /** Safe wrapper around fgets() which aborts on errors * or EOF and prints a suitable error message to help debugging. * diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index addc8bc244..2d86fa2663 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -10,6 +10,22 @@ set_tests_properties(RunLammps PROPERTIES ENVIRONMENT "TSAN_OPTIONS=ignore_noninstrumented_modules=1" PASS_REGULAR_EXPRESSION "^LAMMPS \\([0-9]+ [A-Za-z]+ 2[0-9][0-9][0-9]\\)") +# check if the compiled executable will print the help message +add_test(NAME HelpMessage + COMMAND $ -h + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +set_tests_properties(HelpMessage PROPERTIES + ENVIRONMENT "TSAN_OPTIONS=ignore_noninstrumented_modules=1" + PASS_REGULAR_EXPRESSION ".*Large-scale Atomic/Molecular Massively Parallel Simulator -.*Usage example:.*") + +# check if the compiled executable will error out on an invalid command line flag +add_test(NAME InvalidFlag + COMMAND $ -xxx + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +set_tests_properties(InvalidFlag PROPERTIES + ENVIRONMENT "TSAN_OPTIONS=ignore_noninstrumented_modules=1" + PASS_REGULAR_EXPRESSION "ERROR: Invalid command-line argument.*") + if(BUILD_MPI) function(add_mpi_test) set(MPI_TEST_NUM_PROCS 1) diff --git a/unittest/commands/test_simple_commands.cpp b/unittest/commands/test_simple_commands.cpp index e8cebe98e2..ac8317b3da 100644 --- a/unittest/commands/test_simple_commands.cpp +++ b/unittest/commands/test_simple_commands.cpp @@ -14,6 +14,7 @@ #include "lammps.h" #include "citeme.h" +#include "comm.h" #include "force.h" #include "info.h" #include "input.h" @@ -25,6 +26,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "../testing/core.h" +#include "../testing/utils.h" #include #include @@ -174,6 +176,38 @@ TEST_F(SimpleCommandsTest, Partition) ASSERT_THAT(text, StrEq("")); } +TEST_F(SimpleCommandsTest, Processors) +{ + // default setting is "*" for all dimensions + ASSERT_EQ(lmp->comm->user_procgrid[0], 0); + ASSERT_EQ(lmp->comm->user_procgrid[1], 0); + ASSERT_EQ(lmp->comm->user_procgrid[2], 0); + + BEGIN_HIDE_OUTPUT(); + command("processors 1 1 1"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->comm->user_procgrid[0], 1); + ASSERT_EQ(lmp->comm->user_procgrid[1], 1); + ASSERT_EQ(lmp->comm->user_procgrid[2], 1); + + BEGIN_HIDE_OUTPUT(); + command("processors * 1 *"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->comm->user_procgrid[0], 0); + ASSERT_EQ(lmp->comm->user_procgrid[1], 1); + ASSERT_EQ(lmp->comm->user_procgrid[2], 0); + + BEGIN_HIDE_OUTPUT(); + command("processors 0 0 0"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->comm->user_procgrid[0], 0); + ASSERT_EQ(lmp->comm->user_procgrid[1], 0); + ASSERT_EQ(lmp->comm->user_procgrid[2], 0); + + TEST_FAILURE(".*ERROR: Illegal processors command .*", command("processors -1 0 0");); + TEST_FAILURE(".*ERROR: Specified processors != physical processors.*", command("processors 100 100 100");); +} + TEST_F(SimpleCommandsTest, Quit) { BEGIN_HIDE_OUTPUT(); diff --git a/unittest/commands/test_variables.cpp b/unittest/commands/test_variables.cpp index f31959c3ff..663a2a78a1 100644 --- a/unittest/commands/test_variables.cpp +++ b/unittest/commands/test_variables.cpp @@ -105,8 +105,8 @@ protected: "# comments only\n five\n#END\n", fp); fclose(fp); - fp = fopen("test_variable.atomfile", "w"); + fp = fopen("test_variable.atomfile", "w"); fputs("# test file for atomfile style variable\n\n" "4 # four lines\n4 0.5 #with comment\n" "2 -0.5 \n3 1.5\n1 -1.5\n\n" diff --git a/unittest/cplusplus/CMakeLists.txt b/unittest/cplusplus/CMakeLists.txt index 90ef9a0282..aef69f3722 100644 --- a/unittest/cplusplus/CMakeLists.txt +++ b/unittest/cplusplus/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(test_lammps_class test_lammps_class.cpp) target_link_libraries(test_lammps_class PRIVATE lammps GTest::GMockMain GTest::GTest GTest::GMock) add_test(LammpsClass test_lammps_class) +set_tests_properties(LammpsClass PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1") add_executable(test_input_class test_input_class.cpp) target_link_libraries(test_input_class PRIVATE lammps GTest::GTest GTest::GTestMain) diff --git a/unittest/cplusplus/test_lammps_class.cpp b/unittest/cplusplus/test_lammps_class.cpp index c20cdf336a..0e9c6aa9ab 100644 --- a/unittest/cplusplus/test_lammps_class.cpp +++ b/unittest/cplusplus/test_lammps_class.cpp @@ -1,13 +1,17 @@ // unit tests for the LAMMPS base class +#include "comm.h" +#include "info.h" #include "lammps.h" -#include // for stdin, stdout +#include // for stdin, stdout +#include // for setenv #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" +using ::testing::MatchesRegex; using ::testing::StartsWith; namespace LAMMPS_NS { @@ -95,6 +99,7 @@ TEST_F(LAMMPS_plain, InitMembers) EXPECT_STREQ(LAMMPS::git_branch, "(unknown)"); EXPECT_STREQ(LAMMPS::git_descriptor, "(unknown)"); } + EXPECT_EQ(lmp->comm->nthreads, 1); } TEST_F(LAMMPS_plain, TestStyles) @@ -229,6 +234,9 @@ TEST_F(LAMMPS_omp, InitMembers) EXPECT_STREQ(LAMMPS::git_branch, "(unknown)"); EXPECT_STREQ(LAMMPS::git_descriptor, "(unknown)"); } +#if 0 // temporarily disabled. MacOS behaves different from Linux here. + EXPECT_EQ(lmp->comm->nthreads, 2); +#endif } // test fixture for Kokkos tests @@ -318,10 +326,49 @@ TEST_F(LAMMPS_kokkos, InitMembers) } } -// check help message printing -TEST(LAMMPS_help, HelpMessage) +// check if Comm::nthreads is initialized to either 1 or 2 (from the previous tests) +TEST(LAMMPS_init, OpenMP) { - const char *args[] = {"LAMMPS_test", "-h"}; + if (!LAMMPS::is_installed_pkg("USER-OMP")) GTEST_SKIP(); + + FILE *fp = fopen("in.lammps_empty", "w"); + fputs("\n", fp); + fclose(fp); + + const char *args[] = {"LAMMPS_init", "-in", "in.lammps_empty", "-log", "none", "-nocite"}; + char **argv = (char **)args; + int argc = sizeof(args) / sizeof(char *); + + ::testing::internal::CaptureStdout(); + LAMMPS *lmp = new LAMMPS(argc, argv, MPI_COMM_WORLD); + std::string output = ::testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, MatchesRegex(".*using 2 OpenMP thread.*per MPI task.*")); + + if (LAMMPS_NS::Info::has_accelerator_feature("USER-OMP", "api", "openmp")) + EXPECT_EQ(lmp->comm->nthreads, 2); + else + EXPECT_EQ(lmp->comm->nthreads, 1); + ::testing::internal::CaptureStdout(); + delete lmp; + ::testing::internal::GetCapturedStdout(); + + remove("in.lammps_empty"); +} + +// check no OMP_NUM_THREADS warning message printing. this must be the +// last OpenMP related test as threads will be locked to 1 from here on. + +TEST(LAMMPS_init, NoOpenMP) +{ + if (!LAMMPS_NS::Info::has_accelerator_feature("USER-OMP", "api", "openmp")) + GTEST_SKIP() << "No threading enabled"; + + FILE *fp = fopen("in.lammps_class_noomp", "w"); + fputs("\n", fp); + fclose(fp); + unsetenv("OMP_NUM_THREADS"); + + const char *args[] = {"LAMMPS_init", "-in", "in.lammps_class_noomp", "-log", "none", "-nocite"}; char **argv = (char **)args; int argc = sizeof(args) / sizeof(char *); @@ -329,7 +376,11 @@ TEST(LAMMPS_help, HelpMessage) LAMMPS *lmp = new LAMMPS(argc, argv, MPI_COMM_WORLD); std::string output = ::testing::internal::GetCapturedStdout(); EXPECT_THAT(output, - StartsWith("\nLarge-scale Atomic/Molecular Massively Parallel Simulator -")); + MatchesRegex(".*OMP_NUM_THREADS environment is not set.*Defaulting to 1 thread.*")); + EXPECT_EQ(lmp->comm->nthreads, 1); + ::testing::internal::CaptureStdout(); delete lmp; + ::testing::internal::GetCapturedStdout(); } + } // namespace LAMMPS_NS diff --git a/unittest/force-styles/CMakeLists.txt b/unittest/force-styles/CMakeLists.txt index 60e83e7e16..2c0977397a 100644 --- a/unittest/force-styles/CMakeLists.txt +++ b/unittest/force-styles/CMakeLists.txt @@ -33,7 +33,7 @@ else() target_link_libraries(style_tests PUBLIC mpi_stubs) endif() # propagate sanitizer options to test tools -if (NOT ENABLE_SANITIZER STREQUAL "none") +if (ENABLE_SANITIZER AND (NOT ENABLE_SANITIZER STREQUAL "none")) if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.13) target_compile_options(style_tests PUBLIC -fsanitize=${ENABLE_SANITIZER}) target_link_options(style_tests PUBLIC -fsanitize=${ENABLE_SANITIZER}) diff --git a/unittest/force-styles/tests/atomic-pair-yukawa_colloid.yaml b/unittest/force-styles/tests/atomic-pair-yukawa_colloid.yaml index 1529cf51b9..1c5c24832d 100644 --- a/unittest/force-styles/tests/atomic-pair-yukawa_colloid.yaml +++ b/unittest/force-styles/tests/atomic-pair-yukawa_colloid.yaml @@ -1,7 +1,7 @@ --- lammps_version: 10 Feb 2021 date_generated: Fri Feb 26 23:09:10 2021 -epsilon: 5e-14 +epsilon: 2e-13 prerequisites: ! | atom sphere pair yukawa/colloid diff --git a/unittest/formats/CMakeLists.txt b/unittest/formats/CMakeLists.txt index 4c6de98729..b4c637edfb 100644 --- a/unittest/formats/CMakeLists.txt +++ b/unittest/formats/CMakeLists.txt @@ -28,6 +28,11 @@ if(PKG_MANYBODY) set_tests_properties(EIMPotentialFileReader PROPERTIES ENVIRONMENT "LAMMPS_POTENTIALS=${LAMMPS_POTENTIALS_DIR}") endif() +add_executable(test_text_file_reader test_text_file_reader.cpp) +target_link_libraries(test_text_file_reader PRIVATE lammps GTest::GMock GTest::GTest) +add_test(NAME TextFileReader COMMAND test_text_file_reader WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +set_tests_properties(TextFileReader PROPERTIES ENVIRONMENT "LAMMPS_POTENTIALS=${LAMMPS_POTENTIALS_DIR}") + add_executable(test_file_operations test_file_operations.cpp) target_link_libraries(test_file_operations PRIVATE lammps GTest::GMock GTest::GTest) add_test(NAME FileOperations COMMAND test_file_operations WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/unittest/formats/test_dump_cfg.cpp b/unittest/formats/test_dump_cfg.cpp index b8f879de6f..64930c3ac6 100644 --- a/unittest/formats/test_dump_cfg.cpp +++ b/unittest/formats/test_dump_cfg.cpp @@ -75,6 +75,31 @@ TEST_F(DumpCfgTest, run0) delete_file("dump_cfg_run0.melt.cfg"); } +TEST_F(DumpCfgTest, write_dump) +{ + auto dump_file = "dump_cfg_run*.melt.cfg"; + auto fields = "mass type xs ys zs id proc procp1 x y z ix iy iz vx vy vz fx fy fz"; + + BEGIN_HIDE_OUTPUT(); + command(std::string("write_dump all cfg dump_cfg.melt.cfg ") + fields); + command(std::string("write_dump all cfg dump_cfg*.melt.cfg ") + fields); + END_HIDE_OUTPUT(); + + ASSERT_FILE_EXISTS("dump_cfg.melt.cfg"); + auto lines = read_lines("dump_cfg.melt.cfg"); + ASSERT_EQ(lines.size(), 124); + ASSERT_THAT(lines[0], Eq("Number of particles = 32")); + delete_file("dump_cfg.melt.cfg"); + + ASSERT_FILE_EXISTS("dump_cfg0.melt.cfg"); + lines = read_lines("dump_cfg0.melt.cfg"); + ASSERT_EQ(lines.size(), 124); + ASSERT_THAT(lines[0], Eq("Number of particles = 32")); + delete_file("dump_cfg0.melt.cfg"); + + TEST_FAILURE(".*ERROR: Unrecognized dump style 'xxx'.*", command("write_dump all xxx test.xxx");); +} + TEST_F(DumpCfgTest, unwrap_run0) { auto dump_file = "dump_cfg_unwrap_run*.melt.cfg"; diff --git a/unittest/formats/test_file_operations.cpp b/unittest/formats/test_file_operations.cpp index 79fac5f36e..68311964c4 100644 --- a/unittest/formats/test_file_operations.cpp +++ b/unittest/formats/test_file_operations.cpp @@ -12,11 +12,14 @@ ------------------------------------------------------------------------- */ #include "../testing/core.h" +#include "../testing/utils.h" +#include "atom.h" +#include "domain.h" #include "error.h" #include "info.h" #include "input.h" #include "lammps.h" -#include "utils.h" +#include "update.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -45,19 +48,29 @@ protected: LAMMPSTest::SetUp(); ASSERT_NE(lmp, nullptr); - FILE *fp = fopen("safe_file_read_test.txt", "wb"); - ASSERT_NE(fp, nullptr); - fputs("one line\n", fp); - fputs("two_lines\n", fp); - fputs("\n", fp); - fputs("no newline", fp); - fclose(fp); + std::ofstream out("safe_file_read_test.txt", std::ios_base::out | std::ios_base::binary); + ASSERT_TRUE(out.good()); + out << "one line\ntwo_lines\n\nno newline"; + out.close(); + + out.open("file_with_long_lines_test.txt", std::ios_base::out | std::ios_base::binary); + ASSERT_TRUE(out.good()); + out << "zero ##########################################################" + "##################################################################" + "##################################################################" + "############################################################\n"; + out << "one line\ntwo_lines\n\n"; + for (int i = 0; i < 100; ++i) out << "one two "; + out << "\nthree\nfour five #"; + for (int i = 0; i < 1000; ++i) out << '#'; + out.close(); } void TearDown() override { LAMMPSTest::TearDown(); remove("safe_file_read_test.txt"); + remove("file_with_long_lines_test.txt"); } }; @@ -66,7 +79,7 @@ TEST_F(FileOperationsTest, safe_fgets) { char buf[MAX_BUF_SIZE]; - FILE *fp = fopen("safe_file_read_test.txt", "r"); + FILE *fp = fopen("safe_file_read_test.txt", "rb"); ASSERT_NE(fp, nullptr); memset(buf, 0, MAX_BUF_SIZE); @@ -97,12 +110,74 @@ TEST_F(FileOperationsTest, safe_fgets) fclose(fp); } +#define MAX_BUF_SIZE 128 +TEST_F(FileOperationsTest, fgets_trunc) +{ + char buf[MAX_BUF_SIZE]; + char *ptr; + + FILE *fp = fopen("safe_file_read_test.txt", "rb"); + ASSERT_NE(fp, nullptr); + + memset(buf, 0, MAX_BUF_SIZE); + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_THAT(buf, StrEq("one line\n")); + ASSERT_NE(ptr,nullptr); + + memset(buf, 0, MAX_BUF_SIZE); + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_THAT(buf, StrEq("two_lines\n")); + ASSERT_NE(ptr,nullptr); + + memset(buf, 0, MAX_BUF_SIZE); + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_THAT(buf, StrEq("\n")); + ASSERT_NE(ptr,nullptr); + + memset(buf, 0, MAX_BUF_SIZE); + ptr = utils::fgets_trunc(buf, 4, fp); + ASSERT_THAT(buf, StrEq("no\n")); + ASSERT_NE(ptr,nullptr); + + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_EQ(ptr,nullptr); + fclose(fp); + + fp = fopen("file_with_long_lines_test.txt", "r"); + ASSERT_NE(fp,nullptr); + + memset(buf, 0, MAX_BUF_SIZE); + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_NE(ptr,nullptr); + ASSERT_THAT(buf, StrEq("zero ##########################################################" + "###############################################################\n")); + + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_THAT(buf, StrEq("one line\n")); + ASSERT_NE(ptr,nullptr); + + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_THAT(buf, StrEq("two_lines\n")); + ASSERT_NE(ptr,nullptr); + + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_THAT(buf, StrEq("\n")); + ASSERT_NE(ptr,nullptr); + + ptr = utils::fgets_trunc(buf, MAX_BUF_SIZE, fp); + ASSERT_NE(ptr,nullptr); + ASSERT_THAT(buf, StrEq("one two one two one two one two one two one two one two one two " + "one two one two one two one two one two one two one two one tw\n")); + + fclose(fp); +} + #define MAX_BUF_SIZE 128 TEST_F(FileOperationsTest, safe_fread) { char buf[MAX_BUF_SIZE]; - FILE *fp = fopen("safe_file_read_test.txt", "r"); + FILE *fp = fopen("safe_file_read_test.txt", "rb"); ASSERT_NE(fp, nullptr); memset(buf, 0, MAX_BUF_SIZE); @@ -128,7 +203,7 @@ TEST_F(FileOperationsTest, safe_fread) TEST_F(FileOperationsTest, read_lines_from_file) { - char *buf = new char[MAX_BUF_SIZE]; + char *buf = new char[MAX_BUF_SIZE]; FILE *fp = nullptr; MPI_Comm world = MPI_COMM_WORLD; int me, rv; @@ -225,6 +300,191 @@ TEST_F(FileOperationsTest, error_all_one) lmp->error->one("testme.cpp", 10, "exit {} {}", "too");); } +TEST_F(FileOperationsTest, write_restart) +{ + BEGIN_HIDE_OUTPUT(); + command("echo none"); + END_HIDE_OUTPUT(); + TEST_FAILURE(".*ERROR: Write_restart command before simulation box is defined.*", + command("write_restart test.restart");); + + BEGIN_HIDE_OUTPUT(); + command("region box block -2 2 -2 2 -2 2"); + command("create_box 1 box"); + command("create_atoms 1 single 0.0 0.0 0.0"); + command("mass 1 1.0"); + command("reset_timestep 333"); + command("comm_modify cutoff 0.2"); + command("write_restart noinit.restart noinit"); + command("run 0 post no"); + command("write_restart test.restart"); + command("write_restart step*.restart"); + command("write_restart multi-%.restart"); + command("write_restart multi2-%.restart fileper 2"); + command("write_restart multi3-%.restart nfile 1"); + if (info->has_package("MPIIO")) command("write_restart test.restart.mpiio"); + END_HIDE_OUTPUT(); + + ASSERT_FILE_EXISTS("noinit.restart"); + ASSERT_FILE_EXISTS("test.restart"); + ASSERT_FILE_EXISTS("step333.restart"); + ASSERT_FILE_EXISTS("multi-base.restart"); + ASSERT_FILE_EXISTS("multi-0.restart"); + ASSERT_FILE_EXISTS("multi2-base.restart"); + ASSERT_FILE_EXISTS("multi2-0.restart"); + ASSERT_FILE_EXISTS("multi3-base.restart"); + ASSERT_FILE_EXISTS("multi3-0.restart"); + if (info->has_package("MPIIO")) ASSERT_FILE_EXISTS("test.restart.mpiio"); + + if (!info->has_package("MPIIO")) { + TEST_FAILURE(".*ERROR: Writing to MPI-IO filename when MPIIO package is not inst.*", + command("write_restart test.restart.mpiio");); + } else { + TEST_FAILURE(".*ERROR: Restart file MPI-IO output not allowed with % in filename.*", + command("write_restart test.restart-%.mpiio");); + } + + TEST_FAILURE(".*ERROR: Illegal write_restart command.*", command("write_restart");); + TEST_FAILURE(".*ERROR: Illegal write_restart command.*", + command("write_restart test.restart xxxx");); + TEST_FAILURE(".*ERROR on proc 0: Cannot open restart file some_crazy_dir/test.restart:" + " No such file or directory.*", + command("write_restart some_crazy_dir/test.restart");); + BEGIN_HIDE_OUTPUT(); + command("clear"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 0); + ASSERT_EQ(lmp->update->ntimestep, 0); + ASSERT_EQ(lmp->domain->triclinic, 0); + + TEST_FAILURE( + ".*ERROR on proc 0: Cannot open restart file noexist.restart: No such file or directory.*", + command("read_restart noexist.restart");); + + BEGIN_HIDE_OUTPUT(); + command("read_restart step333.restart"); + command("change_box all triclinic"); + command("write_restart triclinic.restart"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 1); + ASSERT_EQ(lmp->update->ntimestep, 333); + ASSERT_EQ(lmp->domain->triclinic, 1); + BEGIN_HIDE_OUTPUT(); + command("clear"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 0); + ASSERT_EQ(lmp->update->ntimestep, 0); + ASSERT_EQ(lmp->domain->triclinic, 0); + BEGIN_HIDE_OUTPUT(); + command("read_restart triclinic.restart"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 1); + ASSERT_EQ(lmp->update->ntimestep, 333); + ASSERT_EQ(lmp->domain->triclinic, 1); + + // clean up + delete_file("noinit.restart"); + delete_file("test.restart"); + delete_file("step333.restart"); + delete_file("multi-base.restart"); + delete_file("multi-0.restart"); + delete_file("multi2-base.restart"); + delete_file("multi2-0.restart"); + delete_file("multi3-base.restart"); + delete_file("multi3-0.restart"); + delete_file("triclinic.restart"); + if (info->has_package("MPIIO")) delete_file("test.restart.mpiio"); +} + +TEST_F(FileOperationsTest, write_data) +{ + BEGIN_HIDE_OUTPUT(); + command("echo none"); + END_HIDE_OUTPUT(); + TEST_FAILURE(".*ERROR: Write_data command before simulation box is defined.*", + command("write_data test.data");); + + BEGIN_HIDE_OUTPUT(); + command("region box block -2 2 -2 2 -2 2"); + command("create_box 2 box"); + command("create_atoms 1 single 0.5 0.0 0.0"); + command("pair_style zero 1.0"); + command("pair_coeff * *"); + command("mass * 1.0"); + command("reset_timestep 333"); + command("write_data noinit.data noinit"); + command("write_data nocoeff.data nocoeff"); + command("run 0 post no"); + command("write_data test.data"); + command("write_data step*.data pair ij"); + command("fix q all property/atom q"); + command("set type 1 charge -0.5"); + command("write_data charge.data"); + command("write_data nofix.data nofix"); + END_HIDE_OUTPUT(); + + ASSERT_FILE_EXISTS("noinit.data"); + ASSERT_EQ(count_lines("noinit.data"), 26); + ASSERT_FILE_EXISTS("test.data"); + ASSERT_EQ(count_lines("test.data"), 26); + ASSERT_FILE_EXISTS("step333.data"); + ASSERT_EQ(count_lines("step333.data"), 27); + ASSERT_FILE_EXISTS("nocoeff.data"); + ASSERT_EQ(count_lines("nocoeff.data"), 21); + ASSERT_FILE_EXISTS("nofix.data"); + ASSERT_EQ(count_lines("nofix.data"), 26); + ASSERT_FILE_EXISTS("charge.data"); + ASSERT_EQ(count_lines("charge.data"), 30); + + TEST_FAILURE(".*ERROR: Illegal write_data command.*", command("write_data");); + TEST_FAILURE(".*ERROR: Illegal write_data command.*", command("write_data test.data xxxx");); + TEST_FAILURE(".*ERROR: Illegal write_data command.*", command("write_data test.data pair xx");); + TEST_FAILURE(".*ERROR on proc 0: Cannot open data file some_crazy_dir/test.data:" + " No such file or directory.*", + command("write_data some_crazy_dir/test.data");); + + BEGIN_HIDE_OUTPUT(); + command("clear"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->domain->box_exist, 0); + ASSERT_EQ(lmp->atom->natoms, 0); + ASSERT_EQ(lmp->update->ntimestep, 0); + ASSERT_EQ(lmp->domain->triclinic, 0); + + TEST_FAILURE(".*ERROR: Cannot open file noexist.data: No such file or directory.*", + command("read_data noexist.data");); + + BEGIN_HIDE_OUTPUT(); + command("pair_style zero 1.0"); + command("read_data step333.data"); + command("change_box all triclinic"); + command("write_data triclinic.data"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 1); + ASSERT_EQ(lmp->update->ntimestep, 0); + ASSERT_EQ(lmp->domain->triclinic, 1); + BEGIN_HIDE_OUTPUT(); + command("clear"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 0); + ASSERT_EQ(lmp->domain->triclinic, 0); + BEGIN_HIDE_OUTPUT(); + command("pair_style zero 1.0"); + command("read_data triclinic.data"); + END_HIDE_OUTPUT(); + ASSERT_EQ(lmp->atom->natoms, 1); + ASSERT_EQ(lmp->domain->triclinic, 1); + + // clean up + delete_file("charge.data"); + delete_file("nocoeff.data"); + delete_file("noinit.data"); + delete_file("nofix.data"); + delete_file("test.data"); + delete_file("step333.data"); + delete_file("triclinic.data"); +} + int main(int argc, char **argv) { MPI_Init(&argc, &argv); diff --git a/unittest/formats/test_text_file_reader.cpp b/unittest/formats/test_text_file_reader.cpp new file mode 100644 index 0000000000..3294881352 --- /dev/null +++ b/unittest/formats/test_text_file_reader.cpp @@ -0,0 +1,182 @@ +/* ---------------------------------------------------------------------- + LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator + https://lammps.sandia.gov/, Sandia National Laboratories + Steve Plimpton, sjplimp@sandia.gov + + Copyright (2003) Sandia Corporation. Under the terms of Contract + DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains + certain rights in this software. This software is distributed under + the GNU General Public License. + + See the README file in the top-level LAMMPS directory. +------------------------------------------------------------------------- */ + +#include "info.h" +#include "input.h" +#include "text_file_reader.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "../testing/core.h" + +#include +#include +#include +#include + +using namespace LAMMPS_NS; +using LAMMPS_NS::utils::split_words; + +// whether to print verbose output (i.e. not capturing LAMMPS screen output). +bool verbose = false; + +class TextFileReaderTest : public ::testing::Test { + +protected: + void TearDown() override + { + unlink("text_reader_one.file"); + unlink("text_reader_two.file"); + } + + void test_files() + { + FILE *fp = fopen("text_reader_one.file", "w"); + fputs("# test file 1 for text file reader\n\n\none\n two \n\n" + "three # with comment\nfour ! with non-comment\n" + "# comments only\n five\n#END\n", + fp); + fclose(fp); + + fp = fopen("text_reader_two.file", "w"); + fputs("# test file for atomfile style variable\n\n" + "4 # four lines\n4 0.5 #with comment\n" + "2 -0.5 \n3 1.5\n1 -1.5\n\n" + "2\n10 1.0 # test\n13 1.0\n\n######\n" + "4\n1 4.0 # test\n2 3.0\n3 2.0\n4 1.0\n#END\n", + fp); + fclose(fp); + } +}; + +TEST_F(TextFileReaderTest, nofile) +{ + ASSERT_THROW({ TextFileReader reader("text_reader_noexist.file", "test"); }, + FileReaderException); +} + +TEST_F(TextFileReaderTest, permissions) +{ + FILE *fp = fopen("text_reader_noperms.file", "w"); + fputs("word\n", fp); + fclose(fp); + chmod("text_reader_noperms.file", 0); + ASSERT_THROW({ TextFileReader reader("text_reader_noperms.file", "test"); }, + FileReaderException); + unlink("text_reader_noperms.file"); +} + +TEST_F(TextFileReaderTest, nofp) +{ + ASSERT_THROW({ TextFileReader reader(nullptr, "test"); }, + FileReaderException); +} + +TEST_F(TextFileReaderTest, usefp) +{ + test_files(); + FILE *fp = fopen("text_reader_two.file","r"); + ASSERT_NE(fp,nullptr); + + auto reader = new TextFileReader(fp, "test"); + auto line = reader->next_line(); + ASSERT_STREQ(line, "4 "); + line = reader->next_line(1); + ASSERT_STREQ(line, "4 0.5 "); + ASSERT_NO_THROW({ reader->skip_line(); }); + auto values = reader->next_values(1); + ASSERT_EQ(values.count(), 2); + ASSERT_EQ(values.next_int(), 3); + ASSERT_STREQ(values.next_string().c_str(), "1.5"); + ASSERT_NE(reader->next_line(), nullptr); + double data[20]; + ASSERT_THROW({ reader->next_dvector(data,20); }, FileReaderException); + ASSERT_THROW({ reader->skip_line(); }, EOFException); + ASSERT_EQ(reader->next_line(), nullptr); + delete reader; + + // check that we reached EOF and the destructor didn't close the file. + ASSERT_EQ(feof(fp),1); + ASSERT_EQ(fclose(fp),0); +} + +TEST_F(TextFileReaderTest, comments) +{ + test_files(); + TextFileReader reader("text_reader_two.file", "test"); + reader.ignore_comments = true; + auto line = reader.next_line(); + ASSERT_STREQ(line, "4 "); + line = reader.next_line(1); + ASSERT_STREQ(line, "4 0.5 "); + ASSERT_NO_THROW({ reader.skip_line(); }); + auto values = reader.next_values(1); + ASSERT_EQ(values.count(), 2); + ASSERT_EQ(values.next_int(), 3); + ASSERT_STREQ(values.next_string().c_str(), "1.5"); + ASSERT_NE(reader.next_line(), nullptr); + double data[20]; + ASSERT_THROW({ reader.next_dvector(data,20); }, FileReaderException); + ASSERT_THROW({ reader.skip_line(); }, EOFException); + ASSERT_EQ(reader.next_line(), nullptr); +} + +TEST_F(TextFileReaderTest, nocomments) +{ + test_files(); + TextFileReader reader("text_reader_one.file", "test"); + reader.ignore_comments = false; + auto line = reader.next_line(); + ASSERT_STREQ(line, "# test file 1 for text file reader\n"); + line = reader.next_line(1); + ASSERT_STREQ(line, "one\n"); + ASSERT_NO_THROW({ reader.skip_line(); }); + auto values = reader.next_values(4); + ASSERT_EQ(values.count(), 4); + ASSERT_STREQ(values.next_string().c_str(), "three"); + ASSERT_STREQ(values.next_string().c_str(), "#"); + ASSERT_STREQ(values.next_string().c_str(), "with"); + try { + reader.next_values(100); + FAIL() << "No exception thrown\n"; + } catch (EOFException &e) { + ASSERT_STREQ(e.what(), "Incorrect format in test file! 9/100 parameters"); + } + ASSERT_THROW({ reader.skip_line(); }, EOFException); + ASSERT_EQ(reader.next_line(), nullptr); +} + +int main(int argc, char **argv) +{ + MPI_Init(&argc, &argv); + ::testing::InitGoogleMock(&argc, argv); + + if (Info::get_mpi_vendor() == "Open MPI" && !LAMMPS_NS::Info::has_exceptions()) + std::cout << "Warning: using OpenMPI without exceptions. " + "Death tests will be skipped\n"; + + // handle arguments passed via environment variable + if (const char *var = getenv("TEST_ARGS")) { + std::vector env = split_words(var); + for (auto arg : env) { + if (arg == "-v") { + verbose = true; + } + } + } + if ((argc > 1) && (strcmp(argv[1], "-v") == 0)) verbose = true; + + int rv = RUN_ALL_TESTS(); + MPI_Finalize(); + return rv; +} diff --git a/unittest/testing/utils.h b/unittest/testing/utils.h index c76b37872a..730c6fdb44 100644 --- a/unittest/testing/utils.h +++ b/unittest/testing/utils.h @@ -10,14 +10,12 @@ See the README file in the top-level LAMMPS directory. ------------------------------------------------------------------------- */ -#ifndef TEST_EXTENSIONS__H -#define TEST_EXTENSIONS__H +#ifndef LMP_TESTING_UTILS_H +#define LMP_TESTING_UTILS_H #include #include #include -#include -#include #include static void delete_file(const std::string &filename) @@ -65,8 +63,8 @@ static std::vector read_lines(const std::string &filename) static bool file_exists(const std::string &filename) { - struct stat result; - return stat(filename.c_str(), &result) == 0; + std::ifstream infile(filename); + return infile.good(); } #define ASSERT_FILE_EXISTS(NAME) ASSERT_TRUE(file_exists(NAME)) diff --git a/unittest/utils/test_tokenizer.cpp b/unittest/utils/test_tokenizer.cpp index ec097abf62..275a86a05f 100644 --- a/unittest/utils/test_tokenizer.cpp +++ b/unittest/utils/test_tokenizer.cpp @@ -53,6 +53,11 @@ TEST(Tokenizer, skip) ASSERT_FALSE(t.has_next()); ASSERT_EQ(t.count(), 2); ASSERT_THROW(t.skip(), TokenizerException); + try { + t.skip(); + } catch (TokenizerException &e) { + ASSERT_STREQ(e.what(), "No more tokens"); + } } TEST(Tokenizer, prefix_separators) @@ -87,6 +92,47 @@ TEST(Tokenizer, copy_constructor) ASSERT_EQ(u.count(), 2); } +TEST(Tokenizer, move_constructor) +{ + Tokenizer u = std::move(Tokenizer("test new word ", " ")); + ASSERT_THAT(u.next(), Eq("test")); + ASSERT_THAT(u.next(), Eq("new")); + ASSERT_THAT(u.next(), Eq("word")); + ASSERT_EQ(u.count(), 3); +} + +TEST(Tokenizer, copy_assignment) +{ + Tokenizer t(" test word ", " "); + Tokenizer u(" test2 word2 other2 ", " "); + ASSERT_THAT(t.next(), Eq("test")); + ASSERT_THAT(t.next(), Eq("word")); + ASSERT_EQ(t.count(), 2); + Tokenizer v = u; + u = t; + ASSERT_THAT(u.next(), Eq("test")); + ASSERT_THAT(u.next(), Eq("word")); + ASSERT_EQ(u.count(), 2); + + ASSERT_THAT(v.next(), Eq("test2")); + ASSERT_THAT(v.next(), Eq("word2")); + ASSERT_THAT(v.next(), Eq("other2")); + ASSERT_EQ(v.count(), 3); +} + +TEST(Tokenizer, move_assignment) +{ + Tokenizer t(" test word ", " "); + ASSERT_THAT(t.next(), Eq("test")); + ASSERT_THAT(t.next(), Eq("word")); + ASSERT_EQ(t.count(), 2); + t = Tokenizer("test new word ", " "); + ASSERT_THAT(t.next(), Eq("test")); + ASSERT_THAT(t.next(), Eq("new")); + ASSERT_THAT(t.next(), Eq("word")); + ASSERT_EQ(t.count(), 3); +} + TEST(Tokenizer, no_separator_path) { Tokenizer t("one", ":"); @@ -181,12 +227,72 @@ TEST(ValueTokenizer, skip) ASSERT_FALSE(t.has_next()); ASSERT_EQ(t.count(), 2); ASSERT_THROW(t.skip(), TokenizerException); + try { + t.skip(); + } catch (TokenizerException &e) { + ASSERT_STREQ(e.what(), "No more tokens"); + } +} + +TEST(ValueTokenizer, copy_constructor) +{ + ValueTokenizer t(" test word ", " "); + ASSERT_THAT(t.next_string(), Eq("test")); + ASSERT_THAT(t.next_string(), Eq("word")); + ASSERT_EQ(t.count(), 2); + ValueTokenizer u(t); + ASSERT_THAT(u.next_string(), Eq("test")); + ASSERT_THAT(u.next_string(), Eq("word")); + ASSERT_EQ(u.count(), 2); +} + +TEST(ValueTokenizer, move_constructor) +{ + ValueTokenizer u = std::move(ValueTokenizer(" test new word ", " ")); + ASSERT_THAT(u.next_string(), Eq("test")); + ASSERT_THAT(u.next_string(), Eq("new")); + ASSERT_THAT(u.next_string(), Eq("word")); + ASSERT_EQ(u.count(), 3); +} + +TEST(ValueTokenizer, copy_assignment) +{ + ValueTokenizer t(" test word ", " "); + ValueTokenizer u(" test2 word2 other2 ", " "); + ASSERT_THAT(t.next_string(), Eq("test")); + ASSERT_THAT(t.next_string(), Eq("word")); + ASSERT_EQ(t.count(), 2); + ValueTokenizer v = u; + u = t; + ASSERT_THAT(u.next_string(), Eq("test")); + ASSERT_THAT(u.next_string(), Eq("word")); + ASSERT_EQ(u.count(), 2); + + ASSERT_THAT(v.next_string(), Eq("test2")); + ASSERT_THAT(v.next_string(), Eq("word2")); + ASSERT_THAT(v.next_string(), Eq("other2")); + ASSERT_EQ(v.count(), 3); +} + +TEST(ValueTokenizer, move_assignment) +{ + ValueTokenizer t(" test word ", " "); + ASSERT_THAT(t.next_string(), Eq("test")); + ASSERT_THAT(t.next_string(), Eq("word")); + ASSERT_EQ(t.count(), 2); + t = ValueTokenizer("test new word ", " "); + ASSERT_THAT(t.next_string(), Eq("test")); + ASSERT_THAT(t.next_string(), Eq("new")); + ASSERT_THAT(t.next_string(), Eq("word")); + ASSERT_EQ(t.count(), 3); } TEST(ValueTokenizer, bad_integer) { - ValueTokenizer values("f10"); + ValueTokenizer values("f10 f11 f12"); ASSERT_THROW(values.next_int(), InvalidIntegerException); + ASSERT_THROW(values.next_bigint(), InvalidIntegerException); + ASSERT_THROW(values.next_tagint(), InvalidIntegerException); } TEST(ValueTokenizer, bad_double)