Unverified Commit 436951fb authored by Axel Kohlmeyer's avatar Axel Kohlmeyer Committed by GitHub
Browse files

Merge pull request #2270 from akohlmey/check-test-coverage

Add utility to check for missing force style tests
parents 23a8b343 d0be2194
Loading
Loading
Loading
Loading
+56 −52
Original line number Diff line number Diff line
#!/usr/bin/env python3

from __future__ import print_function
import os, re, sys
from glob import glob
from argparse import ArgumentParser
import os, re, sys

parser = ArgumentParser(prog='check-packages.py',
                        description="Check package table completeness")

parser.add_argument("-v", "--verbose",
                    action='store_const',
                    const=True, default=False,
                    action='store_true',
                    help="Enable verbose output")

parser.add_argument("-d", "--doc",
@@ -20,20 +18,28 @@ parser.add_argument("-s", "--src",

args = parser.parse_args()
verbose = args.verbose
src = args.src
doc = args.doc
src_dir = args.src
doc_dir = args.doc

LAMMPS_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))

if not src_dir:
    src_dir = os.path.join(LAMMPS_DIR , 'src')

if not doc_dir:
    doc_dir = os.path.join(LAMMPS_DIR, 'doc', 'src')

if not args.src or not args.doc:
if not src_dir or not doc_dir:
    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(src_dir):
    sys.exit(f"LAMMPS source path {src_dir} does not exist")

if not os.path.isdir(doc):
    sys.exit("LAMMPS documentation source path %s does not exist" % doc)
if not os.path.isdir(doc_dir):
    sys.exit(f"LAMMPS documentation source path {doc_dir} does not exist")

pkgdirs = glob(os.path.join(src, '[A-Z][A-Z]*'))
pkgdirs = glob(os.path.join(src_dir, '[A-Z][A-Z]*'))
dirs = re.compile(".*/([0-9A-Z-]+)$")
user = re.compile("USER-.*")

@@ -46,52 +52,50 @@ usrpkg = []

for d in pkgdirs:
    pkg = dirs.match(d).group(1)
  if not os.path.isdir(os.path.join(src,pkg)): continue
    if not os.path.isdir(os.path.join(src_dir, pkg)): continue
    if pkg in ['DEPEND','MAKE','STUBS']: continue
    if user.match(pkg):
        usrpkg.append(pkg)
    else:
        stdpkg.append(pkg)

print("Found %d standard and %d user packages" % (len(stdpkg),len(usrpkg)))
print(f"Found {len(stdpkg)} standard and {len(usrpkg)} user packages")

counter = 0
fp = open(os.path.join(doc,'Packages_standard.rst'))

with open(os.path.join(doc_dir, 'Packages_standard.rst')) as fp:
    text = fp.read()
fp.close()
matches = re.findall(':ref:`([A-Z0-9-]+) <[A-Z0-9-]+>`',text,re.MULTILINE)

matches = set(re.findall(':ref:`([A-Z0-9-]+) <[A-Z0-9-]+>`', text, re.MULTILINE))
for p in stdpkg:
  if not p in matches:
    ++counter
    print("Standard package %s missing in Packages_standard.rst"
          % p)
    counter += 1
    print(f"Standard package {p} missing in Packages_standard.rst")

fp = open(os.path.join(doc,'Packages_user.rst'))
with open(os.path.join(doc_dir, 'Packages_user.rst')) as fp:
    text = fp.read()
fp.close()
matches = re.findall(':ref:`([A-Z0-9-]+) <[A-Z0-9-]+>`',text,re.MULTILINE)

matches = set(re.findall(':ref:`([A-Z0-9-]+) <[A-Z0-9-]+>`', text, re.MULTILINE))
for p in usrpkg:
  if not p in matches:
    ++counter
    print("User package %s missing in Packages_user.rst"
          % p)
    counter += 1
    print(f"User package {p} missing in Packages_user.rst")

fp = open(os.path.join(doc,'Packages_details.rst'))
with open(os.path.join(doc_dir, 'Packages_details.rst')) as fp:
    text = fp.read()
fp.close()
matches = re.findall(':ref:`([A-Z0-9]+) <PKG-\\1>`',text,re.MULTILINE)

matches = set(re.findall(':ref:`([A-Z0-9]+) <PKG-\\1>`', text, re.MULTILINE))
for p in stdpkg:
    if not p in matches:
    ++counter
    print("Standard package %s missing in Packages_details.rst"
          % p)
matches = re.findall(':ref:`(USER-[A-Z0-9]+) <PKG-\\1>`',text,re.MULTILINE)
        counter += 1
        print(f"Standard package {p} missing in Packages_details.rst")

matches = set(re.findall(':ref:`(USER-[A-Z0-9]+) <PKG-\\1>`', text, re.MULTILINE))
for p in usrpkg:
    if not p in matches:
    ++counter
    print("User package %s missing in Packages_details.rst"
          % p)
        counter +=1 
        print(f"User package {p} missing in Packages_details.rst")

if counter:
    print("Found %d issue(s) with package lists" % counter)
    print(f"Found {counter} issue(s) with package lists")
+141 −140
Original line number Diff line number Diff line
#!/usr/bin/env python3

from __future__ import print_function
import os, re, sys
from glob import glob
from argparse import ArgumentParser
import os, re, sys

parser = ArgumentParser(prog='check-styles.py',
                        description="Check style table completeness")

parser.add_argument("-v", "--verbose",
                    action='store_const',
                    const=True, default=False,
                    action='store_true',
                    help="Enable verbose output")

parser.add_argument("-d", "--doc",
@@ -20,21 +18,29 @@ parser.add_argument("-s", "--src",

args = parser.parse_args()
verbose = args.verbose
src = args.src
doc = args.doc
src_dir = args.src
doc_dir = args.doc

LAMMPS_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))

if not src_dir:
    src_dir = os.path.join(LAMMPS_DIR , 'src')

if not doc_dir:
    doc_dir = os.path.join(LAMMPS_DIR, 'doc', 'src')

if not args.src or not args.doc:
if not src_dir or not doc_dir:
  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(src_dir):
    sys.exit(f"LAMMPS source path {src_dir} does not exist")

if not os.path.isdir(doc):
    sys.exit("LAMMPS documentation source path %s does not exist" % doc)
if not os.path.isdir(doc_dir):
    sys.exit(f"LAMMPS documentation source path {doc_dir} does not exist")

headers = glob(os.path.join(src, '*', '*.h'))
headers += glob(os.path.join(src, '*.h'))
headers = glob(os.path.join(src_dir, '*', '*.h'))
headers += glob(os.path.join(src_dir, '*.h'))

angle = {}
atom = {}
@@ -54,6 +60,7 @@ reader = {}
region = {}
total = 0

style_pattern = re.compile(r"(.+)Style\((.+),(.+)\)")
upper = re.compile("[A-Z]+")
gpu = re.compile("(.+)/gpu$")
intel = re.compile("(.+)/intel$")
@@ -63,61 +70,54 @@ 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']
def register_style(styles, name, info):
    if name in styles:
        for key, value in info.items():
            styles[name][key] += value
    else:
        list[style] = info
        styles[name] = info

def add_suffix(list,style):
def add_suffix(styles, name):
    suffix = ""
    if list[style]['gpu']:
    if styles[name]['gpu']:
        suffix += 'g'
    if list[style]['intel']:
    if styles[name]['intel']:
        suffix += 'i'
    if list[style]['kokkos']:
    if styles[name]['kokkos']:
        suffix += 'k'
    if list[style]['omp']:
    if styles[name]['omp']:
        suffix += 'o'
    if list[style]['opt']:
    if styles[name]['opt']:
        suffix += 't'
    if suffix:
        return style + ' (' + suffix + ')'
        return f"{name} ({suffix})"
    else:
        return style
        return name

def check_style(filename, dirname, pattern, styles, name, suffix=False, skip=set()):
    with open(os.path.join(dirname, filename)) as f:
        text = f.read()

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():
    for c in styles:
        # known undocumented aliases we need to skip
        if c in skip: continue
        s = c
        if suffix: s = add_suffix(list,c)
        if suffix: s = add_suffix(styles, 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)
            if not styles[c]['removed']:
                print(f"{name} style entry {s} is missing or incomplete in {filename}")
                counter += 1
    return counter

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 header in headers:
    if verbose: print("Checking ", header)
    with open(header) as f:
        for line in f:
            matches = style_pattern.findall(line)
            for m in matches:

                # skip over internal styles w/o explicit documentation
                style = m[1]
                total += 1
@@ -202,44 +202,45 @@ print("""Parsed style names w/o suffixes from C++ tree in %s:
   Reader styles:    %3d    Region styles:    %3d
----------------------------------------------------
Total number of styles (including suffixes): %d""" \
      % (src, len(angle), len(atom), len(body), len(bond),   \
      % (src_dir, len(angle), len(atom), len(body), len(bond),   \
       len(command), len(compute), len(dihedral), len(dump), \
       len(fix), len(improper), len(integrate), len(kspace), \
       len(minimize), len(pair), len(reader), len(region), total))


counter = 0

counter += check_style('Commands_all.rst',doc,":doc:`(.+) <.+>`",
counter += check_style('Commands_all.rst', doc_dir, ":doc:`(.+) <.+>`",
                       command,'Command',suffix=False)
counter += check_style('Commands_compute.rst',doc,":doc:`(.+) <compute.+>`",
counter += check_style('Commands_compute.rst', doc_dir, ":doc:`(.+) <compute.+>`",
                       compute,'Compute',suffix=True)
counter += check_style('compute.rst',doc,":doc:`(.+) <compute.+>` -",
counter += check_style('compute.rst', doc_dir, ":doc:`(.+) <compute.+>` -",
                       compute,'Compute',suffix=False)
counter += check_style('Commands_fix.rst',doc,":doc:`(.+) <fix.+>`",
                       fix,'Fix',skip=('python'),suffix=True)
counter += check_style('fix.rst',doc,":doc:`(.+) <fix.+>` -",
                       fix,'Fix',skip=('python'),suffix=False)
counter += check_style('Commands_pair.rst',doc,":doc:`(.+) <pair.+>`",
counter += check_style('Commands_fix.rst', doc_dir, ":doc:`(.+) <fix.+>`",
                       fix,'Fix',skip=('python', 'NEIGH_HISTORY/omp'),suffix=True)
counter += check_style('fix.rst', doc_dir, ":doc:`(.+) <fix.+>` -",
                       fix,'Fix',skip=('python', 'NEIGH_HISTORY/omp'),suffix=False)
counter += check_style('Commands_pair.rst', doc_dir, ":doc:`(.+) <pair.+>`",
                       pair,'Pair',skip=('meam','lj/sf'),suffix=True)
counter += check_style('pair_style.rst',doc,":doc:`(.+) <pair.+>` -",
counter += check_style('pair_style.rst', doc_dir, ":doc:`(.+) <pair.+>` -",
                       pair,'Pair',skip=('meam','lj/sf'),suffix=False)
counter += check_style('Commands_bond.rst',doc,":doc:`(.+) <bond.+>`",
counter += check_style('Commands_bond.rst', doc_dir, ":doc:`(.+) <bond.+>`",
                       bond,'Bond',suffix=True)
counter += check_style('bond_style.rst',doc,":doc:`(.+) <bond.+>` -",
counter += check_style('bond_style.rst', doc_dir, ":doc:`(.+) <bond.+>` -",
                       bond,'Bond',suffix=False)
counter += check_style('Commands_bond.rst',doc,":doc:`(.+) <angle.+>`",
counter += check_style('Commands_bond.rst', doc_dir, ":doc:`(.+) <angle.+>`",
                       angle,'Angle',suffix=True)
counter += check_style('angle_style.rst',doc,":doc:`(.+) <angle.+>` -",
counter += check_style('angle_style.rst', doc_dir, ":doc:`(.+) <angle.+>` -",
                       angle,'Angle',suffix=False)
counter += check_style('Commands_bond.rst',doc,":doc:`(.+) <dihedral.+>`",
counter += check_style('Commands_bond.rst', doc_dir, ":doc:`(.+) <dihedral.+>`",
                       dihedral,'Dihedral',suffix=True)
counter += check_style('dihedral_style.rst',doc,":doc:`(.+) <dihedral.+>` -",
counter += check_style('dihedral_style.rst', doc_dir, ":doc:`(.+) <dihedral.+>` -",
                       dihedral,'Dihedral',suffix=False)
counter += check_style('Commands_bond.rst',doc,":doc:`(.+) <improper.+>`",
counter += check_style('Commands_bond.rst', doc_dir, ":doc:`(.+) <improper.+>`",
                       improper,'Improper',suffix=True)
counter += check_style('improper_style.rst',doc,":doc:`(.+) <improper.+>` -",
counter += check_style('improper_style.rst', doc_dir, ":doc:`(.+) <improper.+>` -",
                       improper,'Improper',suffix=False)
counter += check_style('Commands_kspace.rst',doc,":doc:`(.+) <kspace_style>`",
counter += check_style('Commands_kspace.rst', doc_dir, ":doc:`(.+) <kspace_style>`",
                       kspace,'KSpace',suffix=True)

if counter:
+17 −0
Original line number Diff line number Diff line
@@ -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})
+184 −0
Original line number Diff line number Diff line
#!/usr/bin/env python3
import os, re, sys
from glob import glob
from argparse import ArgumentParser

parser = ArgumentParser(prog='check_tests.py',
                        description="Check force tests for completeness")

parser.add_argument("-v", "--verbose",
                    action='store_true',
                    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_dir = args.src
tests_dir = args.tests

LAMMPS_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))

if not src_dir:
    src_dir = os.path.join(LAMMPS_DIR , 'src')

if not tests_dir:
    tests_dir = os.path.join(LAMMPS_DIR, 'unittest', 'force-styles', 'tests')

try:
    src_dir = os.path.abspath(os.path.expanduser(src_dir))
    tests_dir = os.path.abspath(os.path.expanduser(tests_dir))
except:
    parser.print_help()
    sys.exit(1)

if not os.path.isdir(src_dir):
    sys.exit(f"LAMMPS source path {src_dir} does not exist")

if not os.path.isdir(tests_dir):
    sys.exit(f"LAMMPS test inputs path {tests_dir} does not exist")

headers = glob(os.path.join(src_dir, '*', '*.h'))
headers += glob(os.path.join(src_dir, '*.h'))

angle = {}
bond = {}
compute = {}
dihedral = {}
dump = {}
fix = {}
improper = {}
kspace = {}
pair = {}

style_pattern = re.compile("(.+)Style\((.+),(.+)\)")
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(styles, name, info):
    if name in styles:
        styles[name]['gpu'] += info['gpu']
        styles[name]['intel'] += info['intel']
        styles[name]['kokkos'] += info['kokkos']
        styles[name]['omp'] += info['omp']
        styles[name]['opt'] += info['opt']
        styles[name]['removed'] += info['removed']
    else:
        styles[name] = info

print("Parsing style names from C++ tree in: ", src_dir)

for header in headers:
    if verbose: print("Checking ", header)
    with open(header) as f:
        for line in f:
            matches = style_pattern.findall(line)

            for m in matches:
                # skip over internal styles w/o explicit documentation
                style = m[1]
                if style.isupper():
                    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)



def check_tests(name,styles,yaml,search,skip=()):
    yaml_files = glob(os.path.join(tests_dir, yaml))
    tests = set()
    missing = set()
    search_pattern = re.compile(search)
    for y in yaml_files:
        if verbose: print("Checking: ",y)
        with open(y) as f:
            for line in f:
                matches = search_pattern.findall(line)
                for m in matches:
                    if m[1] == 'hybrid' or m[1] == 'hybrid/overlay':
                        for s in m[0].split():
                            if s in styles:
                                tests.add(s)
                    else:
                        tests.add(m[1])
    for s in styles:
        # known undocumented aliases we need to skip
        if s in skip: continue
        if not s in tests:
            if not styles[s]['removed']:
                if verbose: print("No test for %s style %s" % (name,s))
                missing.add(s)

    num_missing = len(missing)
    total = len(styles)
    num_tests = total - num_missing
    print(f"\nTests available for {name} styles: {num_tests} of {total}")
    print("No tests for: ", list(missing))
    return num_missing

counter = 0
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(f"\nTotal tests missing: {counter} of {total}")