#!/usr/bin/env python2 # # This script is written to look through source code files, looking for # missing unit-tests and doc-block comments. It's mostly ugly, but so is # the task at hand. import os import re # This is sort of a monster function, but I've tried to lift as much out # of the logic as I could. Basically, this function tries to find # anything that's "wrong", which is one of: # # - missing docblock comment for functions # - missing test for a function def parse(fname): lines = [] with open(fname, 'r') as f: lines = f.read().split("\n") length = len(lines) # We probably got a directory in the path (like './src'); let's chop # it off. basefname = os.path.basename(fname) # The suite is the name of the test suite as Criterion would define # it. suite = suite_name_from_file(basefname) for num in range(0, length): match = find_func_name(lines[num]) if match is not None: # The test name is a part of the function test_name = test_name_from_func(suite, match.group(1)) # We want to find something in a test file for each of the # functions we define. if not has_test('tests/' + basefname, suite, test_name): print "Missing test: %s_%s" % (suite, test_name) # We also want to have some doc-block comment for every # function we write. if not has_doc_block(lines, num): print "Missing docblock comment: %s:%d: %s" % (fname, num, lines[num]) # Given a source file name, extract the suite name (which is really just # the name of the file minus the '.c' part, and -- well, you can read # the source). def suite_name_from_file(fname): return fname.replace('.c', '').replace('.', '_') # Given a line of source code, figure out if there's a function that's # defined there. We define functions in the form of: # # # (...) # { # # } # # So we can say a function definition will always begin at the first # character on a line in which the name is immediately followed by an # open parenthesis. def find_func_name(line): return re.search(r'^([a-zA-Z_][a-zA-Z0-9_]*)\(', line) # Given a suite and a function name, figure out what the test name # should be. def test_name_from_func(suite, func): return func.replace(suite + '_', '') # Given a line and a line number, determine if we have a doc-block or # not. (Which we can determine because there should be a closing comment # token on the line above the type.) def has_doc_block(lines, linenum): if lines[linenum].find('// ignore docblock'): return True return linenum-2 >= 0 and (lines[linenum-2] == ' */' or lines[linenum-1] == ' */') # Do we have a test for this function? The fname is the test file, and # the suite and test are -- well, what they are. def has_test(fname, suite, test): # This is a bit of fakery, but inspect-c is not (yet) smart enough # to know that a macro like `DEFINE_ADDR(xyz)` expands to # `mos6502_resolve_xyz` and that the test it should associate is # that. So we skip macro function definitions for now. if test.isupper(): return True data = '' with open(fname, 'r') as f: data = f.read() return data.find('Test(%s, %s)' % (suite, test)) > -1 # Walk through a directory, looking for C source files and header files # to examine. def walk(dname): for root, subdirs, subfiles in os.walk(dname): for fname in subfiles: if fname == 'main.c': continue if fname[-2:] != '.c' and fname[-2:] != '.h': continue parse(root + '/' + fname) # Walk through the src dir and see what we can see. walk('./src') # vim:ft=python: