From 896a08883b1354cc4f623b72c039ae0160824a54 Mon Sep 17 00:00:00 2001 From: Axel Kohlmeyer Date: Mon, 3 Aug 2020 00:12:51 -0400 Subject: [PATCH] add a custom python script to check the completeness of tests inputs --- unittest/force-styles/CMakeLists.txt | 17 +++ unittest/force-styles/check_tests.py | 218 +++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100755 unittest/force-styles/check_tests.py diff --git a/unittest/force-styles/CMakeLists.txt b/unittest/force-styles/CMakeLists.txt index 14a53f4e3e..104d37b861 100644 --- a/unittest/force-styles/CMakeLists.txt +++ b/unittest/force-styles/CMakeLists.txt @@ -5,6 +5,23 @@ if(NOT YAML_FOUND) return() endif() +if(CMAKE_VERSION VERSION_LESS 3.12) + # adjust so we find Python 3 versions before Python 2 on old systems with old CMake + set(Python_ADDITIONAL_VERSIONS 3.8 3.7 3.6 3.5) + find_package(PythonInterp) # Deprecated since version 3.12 + if(PYTHONINTERP_FOUND) + set(Python_EXECUTABLE ${PYTHON_EXECUTABLE}) + endif() +else() + find_package(Python COMPONENTS Interpreter) +endif() +if (Python_EXECUTABLE) + add_custom_target(check-tests + ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check_tests.py + -s ${LAMMPS_SOURCE_DIR} -t ${CMAKE_CURRENT_SOURCE_DIR}/tests + COMMENT "Check completeness of force style tests") +endif() + set(TEST_INPUT_FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/tests) add_library(style_tests STATIC yaml_writer.cpp error_stats.cpp test_config_reader.cpp test_main.cpp) target_compile_definitions(style_tests PRIVATE -DTEST_INPUT_FOLDER=${TEST_INPUT_FOLDER}) diff --git a/unittest/force-styles/check_tests.py b/unittest/force-styles/check_tests.py new file mode 100755 index 0000000000..26519a3eca --- /dev/null +++ b/unittest/force-styles/check_tests.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 + +from __future__ import print_function +from glob import glob +from argparse import ArgumentParser +import os, re, sys + +parser = ArgumentParser(prog='check_tests.py', + description="Check force tests for completeness") + +parser.add_argument("-v", "--verbose", + action='store_const', + const=True, default=False, + help="Enable verbose output") + +parser.add_argument("-t", "--tests", + help="Path to LAMMPS test YAML format input files") +parser.add_argument("-s", "--src", + help="Path to LAMMPS sources") + +args = parser.parse_args() +verbose = args.verbose +src = args.src +tests = args.tests + +if not src: + src = os.path.join('.','..','..','src') + +if not tests: + tests = os.path.join(src,'..','unittest','force-styles','tests') + +try: + src = os.path.abspath(os.path.expanduser(src)) + tests = os.path.abspath(os.path.expanduser(tests)) +except: + parser.print_help() + sys.exit(1) + +if not os.path.isdir(src): + sys.exit("LAMMPS source path %s does not exist" % src) + +if not os.path.isdir(tests): + sys.exit("LAMMPS test inputs path %s does not exist" % tests) + +headers = glob(os.path.join(src, '*', '*.h')) +headers += glob(os.path.join(src, '*.h')) + +angle = {} +bond = {} +compute = {} +dihedral = {} +dump = {} +fix = {} +improper = {} +kspace = {} +pair = {} +total = 0 + +upper = re.compile("[A-Z]+") +gpu = re.compile("(.+)/gpu$") +intel = re.compile("(.+)/intel$") +kokkos = re.compile("(.+)/kk$") +kokkos_skip = re.compile("(.+)/kk/(host|device)$") +omp = re.compile("(.+)/omp$") +opt = re.compile("(.+)/opt$") +removed = re.compile("(.*)Deprecated$") + +def register_style(list,style,info): + if style in list.keys(): + list[style]['gpu'] += info['gpu'] + list[style]['intel'] += info['intel'] + list[style]['kokkos'] += info['kokkos'] + list[style]['omp'] += info['omp'] + list[style]['opt'] += info['opt'] + list[style]['removed'] += info['removed'] + else: + list[style] = info + +def add_suffix(list,style): + suffix = "" + if list[style]['gpu']: + suffix += 'g' + if list[style]['intel']: + suffix += 'i' + if list[style]['kokkos']: + suffix += 'k' + if list[style]['omp']: + suffix += 'o' + if list[style]['opt']: + suffix += 't' + if suffix: + return style + ' (' + suffix + ')' + else: + return style + +def check_style(file,dir,pattern,list,name,suffix=False,skip=()): + f = os.path.join(dir, file) + fp = open(f) + text = fp.read() + fp.close() + matches = re.findall(pattern,text,re.MULTILINE) + counter = 0 + for c in list.keys(): + # known undocumented aliases we need to skip + if c in skip: continue + s = c + if suffix: s = add_suffix(list,c) + if not s in matches: + if not list[c]['removed']: + print("%s style entry %s" % (name,s), + "is missing or incomplete in %s" % file) + counter += 1 + return counter + +print("Parsing style names from C++ tree in: ",src) + +for h in headers: + if verbose: print("Checking ", h) + fp = open(h) + text = fp.read() + fp.close() + matches = re.findall("(.+)Style\((.+),(.+)\)",text,re.MULTILINE) + for m in matches: + + # skip over internal styles w/o explicit documentation + style = m[1] + total += 1 + if upper.match(style): + continue + + # detect, process, and flag suffix styles: + info = { 'kokkos': 0, 'gpu': 0, 'intel': 0, \ + 'omp': 0, 'opt': 0, 'removed': 0 } + suffix = kokkos_skip.match(style) + if suffix: + continue + suffix = gpu.match(style) + if suffix: + style = suffix.groups()[0] + info['gpu'] = 1 + suffix = intel.match(style) + if suffix: + style = suffix.groups()[0] + info['intel'] = 1 + suffix = kokkos.match(style) + if suffix: + style = suffix.groups()[0] + info['kokkos'] = 1 + suffix = omp.match(style) + if suffix: + style = suffix.groups()[0] + info['omp'] = 1 + suffix = opt.match(style) + if suffix: + style = suffix.groups()[0] + info['opt'] = 1 + deprecated = removed.match(m[2]) + if deprecated: + info['removed'] = 1 + + # register style and suffix flags + if m[0] == 'Angle': + register_style(angle,style,info) + elif m[0] == 'Bond': + register_style(bond,style,info) + elif m[0] == 'Dihedral': + register_style(dihedral,style,info) + elif m[0] == 'Improper': + register_style(improper,style,info) + elif m[0] == 'KSpace': + register_style(kspace,style,info) + elif m[0] == 'Pair': + register_style(pair,style,info) + + +counter = 0 + +def check_tests(name,list,yaml,search,skip=()): + num = 0 + yaml_files = glob(os.path.join(tests, yaml)) + styles = [] + missing = [] + for y in yaml_files: + if verbose: print("Checking: ",y) + fp = open(y) + text = fp.read() + fp.close() + matches = re.findall(search,text,re.MULTILINE) + for m in matches: + styles.append(m) + for s in list.keys(): + # known undocumented aliases we need to skip + if s in skip: continue + if not s in styles: + if not list[s]['removed']: + if verbose: print("No test for %s style %s" % (name,s)) + num += 1 + missing.append(s) + total = len(list) + print("\nTests for %s styles: %d of %d" % (name,total - num, total)) + print("No tests for: ", missing) + return num + +counter += check_tests('pair',pair,'*-pair-*.yaml', + '.*pair_style:\s*(\S+).*',skip=('meam','lj/sf')) +counter += check_tests('bond',bond,'bond-*.yaml', + '.*bond_style:\s*(\S+).*') +counter += check_tests('angle',angle,'angle-*.yaml', + '.*angle_style:\s*(\S+).*') +counter += check_tests('dihedral',dihedral,'dihedral-*.yaml', + '.*dihedral_style:\s*(\S+).*') +counter += check_tests('improper',improper,'improper-*.yaml', + '.*improper_style:\s*(\S+).*') +counter += check_tests('kspace',kspace,'kspace-*.yaml', + '.*kspace_style\s*(\S+).*') + +total = len(pair)+len(bond)+len(angle)+len(dihedral)+len(improper)+len(kspace) +print("\nTotal tests missing: %d of %d" % (total - counter, total))