Browse Source

Merge remote-tracking branch 'remotes/origin/master'

xieyangrun 4 years ago
parent
commit
4828c87a54
4 changed files with 156 additions and 73 deletions
  1. 14 3
      .github/workflows/release.yml
  2. 11 0
      .github/workflows/test.yml
  3. 0 14
      lfs.c
  4. 131 56
      scripts/test.py

+ 14 - 3
.github/workflows/release.yml

@@ -96,7 +96,7 @@ jobs:
                   | capture("Code size is (?<result>[0-9]+)").result' \
                 prev-results.json || echo 0)"
             ./scripts/code.py -u results/code-thumb-readonly.csv -s | awk '
-              NR==2 {printf "Code size (readonly),%d B",$2}
+              NR==2 {printf "Code size<br/>(readonly),%d B",$2}
               NR==2 && ENVIRON["PREV"]+0 != 0 {
                 printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}
               NR==2 {printf "\n"}' \
@@ -107,7 +107,7 @@ jobs:
                   | capture("Code size is (?<result>[0-9]+)").result' \
                 prev-results.json || echo 0)"
             ./scripts/code.py -u results/code-thumb-threadsafe.csv -s | awk '
-              NR==2 {printf "Code size (threadsafe),%d B",$2}
+              NR==2 {printf "Code size<br/>(threadsafe),%d B",$2}
               NR==2 && ENVIRON["PREV"]+0 != 0 {
                 printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}
               NR==2 {printf "\n"}' \
@@ -118,7 +118,18 @@ jobs:
                   | capture("Code size is (?<result>[0-9]+)").result' \
                 prev-results.json || echo 0)"
             ./scripts/code.py -u results/code-thumb-migrate.csv -s | awk '
-              NR==2 {printf "Code size (migrate),%d B",$2}
+              NR==2 {printf "Code size<br/>(migrate),%d B",$2}
+              NR==2 && ENVIRON["PREV"]+0 != 0 {
+                printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}
+              NR==2 {printf "\n"}' \
+            >> results.csv)
+          [ -e results/code-thumb-error-asserts.csv ] && ( \
+            export PREV="$(jq -re '
+                  select(.context == "results / code (error-asserts)").description
+                  | capture("Code size is (?<result>[0-9]+)").result' \
+                prev-results.json || echo 0)"
+            ./scripts/code.py -u results/code-thumb-error-asserts.csv -s | awk '
+              NR==2 {printf "Code size<br/>(error-asserts),%d B",$2}
               NR==2 && ENVIRON["PREV"]+0 != 0 {
                 printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}
               NR==2 {printf "\n"}' \

+ 11 - 0
.github/workflows/test.yml

@@ -195,6 +195,17 @@ jobs:
               -DLFS_NO_ERROR \
               -DLFS_MIGRATE" \
             CODEFLAGS+="-o results/code-${{matrix.arch}}-migrate.csv"
+      - name: results-code-error-asserts
+        run: |
+          mkdir -p results
+          make clean
+          make code \
+            CFLAGS+=" \
+              -DLFS_NO_DEBUG \
+              -DLFS_NO_WARN \
+              -DLFS_NO_ERROR \
+              -D'LFS_ASSERT(test)=do {if(!(test)) {return -1;}} while(0)'" \
+            CODEFLAGS+="-o results/code-${{matrix.arch}}-error-asserts.csv"
       - name: upload-results
         uses: actions/upload-artifact@v2
         with:

+ 0 - 14
lfs.c

@@ -452,20 +452,6 @@ static void lfs_mlist_append(lfs_t *lfs, struct lfs_mlist *mlist) {
     lfs->mlist = mlist;
 }
 
-static inline void lfs_mlist_remove(lfs_t *lfs, struct lfs_mlist *mlist) {
-    for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) {
-        if (*p == mlist) {
-            *p = (*p)->next;
-            break;
-        }
-    }
-}
-
-static inline void lfs_mlist_append(lfs_t *lfs, struct lfs_mlist *mlist) {
-    mlist->next = lfs->mlist;
-    lfs->mlist = mlist;
-}
-
 
 /// Internal operations predeclared here ///
 #ifndef LFS_READONLY

+ 131 - 56
scripts/test.py

@@ -20,19 +20,50 @@ import pty
 import errno
 import signal
 
-TESTDIR = 'tests'
+TEST_PATHS = 'tests'
 RULES = """
+# add block devices to sources
+TESTSRC ?= $(SRC) $(wildcard bd/*.c)
+
 define FLATTEN
-tests/%$(subst /,.,$(target)): $(target)
+%(path)s%%$(subst /,.,$(target)): $(target)
     ./scripts/explode_asserts.py $$< -o $$@
 endef
-$(foreach target,$(SRC),$(eval $(FLATTEN)))
-
--include tests/*.d
+$(foreach target,$(TESTSRC),$(eval $(FLATTEN)))
 
+-include %(path)s*.d
 .SECONDARY:
-%.test: %.test.o $(foreach f,$(subst /,.,$(SRC:.c=.o)),%.$f)
+
+%(path)s.test: %(path)s.test.o \\
+        $(foreach t,$(subst /,.,$(TESTSRC:.c=.o)),%(path)s.$t)
     $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@
+
+# needed in case builddir is different
+%(path)s%%.o: %(path)s%%.c
+    $(CC) -c -MMD $(CFLAGS) $< -o $@
+"""
+COVERAGE_RULES = """
+%(path)s.test: override CFLAGS += -fprofile-arcs -ftest-coverage
+
+# delete lingering coverage
+%(path)s.test: | %(path)s.info.clean
+.PHONY: %(path)s.info.clean
+%(path)s.info.clean:
+    rm -f %(path)s*.gcda
+
+# accumulate coverage info
+.PHONY: %(path)s.info
+%(path)s.info:
+    $(strip $(LCOV) -c \\
+        $(addprefix -d ,$(wildcard %(path)s*.gcda)) \\
+        --rc 'geninfo_adjust_src_path=$(shell pwd)' \\
+        -o $@)
+    $(LCOV) -e $@ $(addprefix /,$(SRC)) -o $@
+ifdef COVERAGETARGET
+    $(strip $(LCOV) -a $@ \\
+        $(addprefix -a ,$(wildcard $(COVERAGETARGET))) \\
+        -o $(COVERAGETARGET))
+endif
 """
 GLOBALS = """
 //////////////// AUTOGENERATED TEST ////////////////
@@ -119,6 +150,8 @@ class TestCase:
         self.if_ = config.get('if', None)
         self.in_ = config.get('in', None)
 
+        self.result = None
+
     def __str__(self):
         if hasattr(self, 'permno'):
             if any(k not in self.case.defines for k in self.defines):
@@ -179,7 +212,7 @@ class TestCase:
                 len(self.filter) >= 2 and
                 self.filter[1] != self.permno):
             return False
-        elif args.get('no_internal', False) and self.in_ is not None:
+        elif args.get('no_internal') and self.in_ is not None:
             return False
         elif self.if_ is not None:
             if_ = self.if_
@@ -213,7 +246,7 @@ class TestCase:
                 try:
                     with open(disk, 'w') as f:
                         f.truncate(0)
-                    if args.get('verbose', False):
+                    if args.get('verbose'):
                         print('truncate --size=0', disk)
                 except FileNotFoundError:
                     pass
@@ -237,14 +270,14 @@ class TestCase:
                     '-ex', 'r'])
             ncmd.extend(['--args'] + cmd)
 
-            if args.get('verbose', False):
+            if args.get('verbose'):
                 print(' '.join(shlex.quote(c) for c in ncmd))
             signal.signal(signal.SIGINT, signal.SIG_IGN)
             sys.exit(sp.call(ncmd))
 
         # run test case!
         mpty, spty = pty.openpty()
-        if args.get('verbose', False):
+        if args.get('verbose'):
             print(' '.join(shlex.quote(c) for c in cmd))
         proc = sp.Popen(cmd, stdout=spty, stderr=spty)
         os.close(spty)
@@ -260,7 +293,7 @@ class TestCase:
                         break
                     raise
                 stdout.append(line)
-                if args.get('verbose', False):
+                if args.get('verbose'):
                     sys.stdout.write(line)
                 # intercept asserts
                 m = re.match(
@@ -299,7 +332,7 @@ class ValgrindTestCase(TestCase):
         return not self.leaky and super().shouldtest(**args)
 
     def test(self, exec=[], **args):
-        verbose = args.get('verbose', False)
+        verbose = args.get('verbose')
         uninit = (self.defines.get('LFS_ERASE_VALUE', None) == -1)
         exec = [
             'valgrind',
@@ -351,12 +384,17 @@ class TestSuite:
         self.name = os.path.basename(path)
         if self.name.endswith('.toml'):
             self.name = self.name[:-len('.toml')]
-        self.path = path
+        if args.get('build_dir'):
+            self.toml = path
+            self.path = args['build_dir'] + '/' + path
+        else:
+            self.toml = path
+            self.path = path
         self.classes = classes
         self.defines = defines.copy()
         self.filter = filter
 
-        with open(path) as f:
+        with open(self.toml) as f:
             # load tests
             config = toml.load(f)
 
@@ -467,7 +505,7 @@ class TestSuite:
 
     def build(self, **args):
         # build test files
-        tf = open(self.path + '.test.c.t', 'w')
+        tf = open(self.path + '.test.tc', 'w')
         tf.write(GLOBALS)
         if self.code is not None:
             tf.write('#line %d "%s"\n' % (self.code_lineno, self.path))
@@ -477,7 +515,7 @@ class TestSuite:
         for case in self.cases:
             if case.in_ not in tfs:
                 tfs[case.in_] = open(self.path+'.'+
-                    case.in_.replace('/', '.')+'.t', 'w')
+                    re.sub('(\.c)?$', '.tc', case.in_.replace('/', '.')), 'w')
                 tfs[case.in_].write('#line 1 "%s"\n' % case.in_)
                 with open(case.in_) as f:
                     for line in f:
@@ -516,25 +554,33 @@ class TestSuite:
 
         # write makefiles
         with open(self.path + '.mk', 'w') as mk:
-            mk.write(RULES.replace(4*' ', '\t'))
+            mk.write(RULES.replace(4*' ', '\t') % dict(path=self.path))
             mk.write('\n')
 
+            # add coverage hooks?
+            if args.get('coverage'):
+                mk.write(COVERAGE_RULES.replace(4*' ', '\t') % dict(
+                    path=self.path))
+                mk.write('\n')
+
             # add truely global defines globally
             for k, v in sorted(self.defines.items()):
-                mk.write('%s: override CFLAGS += -D%s=%r\n' % (
-                    self.path+'.test', k, v))
+                mk.write('%s.test: override CFLAGS += -D%s=%r\n'
+                    % (self.path, k, v))
 
             for path in tfs:
                 if path is None:
                     mk.write('%s: %s | %s\n' % (
                         self.path+'.test.c',
-                        self.path,
-                        self.path+'.test.c.t'))
+                        self.toml,
+                        self.path+'.test.tc'))
                 else:
                     mk.write('%s: %s %s | %s\n' % (
                         self.path+'.'+path.replace('/', '.'),
-                        self.path, path,
-                        self.path+'.'+path.replace('/', '.')+'.t'))
+                        self.toml,
+                        path,
+                        self.path+'.'+re.sub('(\.c)?$', '.tc',
+                            path.replace('/', '.'))))
                 mk.write('\t./scripts/explode_asserts.py $| -o $@\n')
 
         self.makefile = self.path + '.mk'
@@ -557,7 +603,7 @@ class TestSuite:
                 if not args.get('verbose', True):
                     sys.stdout.write(FAIL)
                     sys.stdout.flush()
-                if not args.get('keep_going', False):
+                if not args.get('keep_going'):
                     if not args.get('verbose', True):
                         sys.stdout.write('\n')
                     raise
@@ -579,30 +625,30 @@ def main(**args):
 
     # and what class of TestCase to run
     classes = []
-    if args.get('normal', False):
+    if args.get('normal'):
         classes.append(TestCase)
-    if args.get('reentrant', False):
+    if args.get('reentrant'):
         classes.append(ReentrantTestCase)
-    if args.get('valgrind', False):
+    if args.get('valgrind'):
         classes.append(ValgrindTestCase)
     if not classes:
         classes = [TestCase]
 
     suites = []
-    for testpath in args['testpaths']:
+    for testpath in args['test_paths']:
         # optionally specified test case/perm
         testpath, *filter = testpath.split('#')
         filter = [int(f) for f in filter]
 
         # figure out the suite's toml file
         if os.path.isdir(testpath):
-            testpath = testpath + '/test_*.toml'
+            testpath = testpath + '/*.toml'
         elif os.path.isfile(testpath):
             testpath = testpath
         elif testpath.endswith('.toml'):
-            testpath = TESTDIR + '/' + testpath
+            testpath = TEST_PATHS + '/' + testpath
         else:
-            testpath = TESTDIR + '/' + testpath + '.toml'
+            testpath = TEST_PATHS + '/' + testpath + '.toml'
 
         # find tests
         for path in glob.glob(testpath):
@@ -628,7 +674,7 @@ def main(**args):
         list(it.chain.from_iterable(['-f', m] for m in makefiles)) +
         [target for target in targets])
     mpty, spty = pty.openpty()
-    if args.get('verbose', False):
+    if args.get('verbose'):
         print(' '.join(shlex.quote(c) for c in cmd))
     proc = sp.Popen(cmd, stdout=spty, stderr=spty)
     os.close(spty)
@@ -642,14 +688,14 @@ def main(**args):
                 break
             raise
         stdout.append(line)
-        if args.get('verbose', False):
+        if args.get('verbose'):
             sys.stdout.write(line)
         # intercept warnings
         m = re.match(
             '^{0}([^:]+):(\d+):(?:\d+:)?{0}{1}:{0}(.*)$'
             .format('(?:\033\[[\d;]*.| )*', 'warning'),
             line)
-        if m and not args.get('verbose', False):
+        if m and not args.get('verbose'):
             try:
                 with open(m.group(1)) as f:
                     lineno = int(m.group(2))
@@ -662,27 +708,26 @@ def main(**args):
             except:
                 pass
     proc.wait()
-
     if proc.returncode != 0:
-        if not args.get('verbose', False):
+        if not args.get('verbose'):
             for line in stdout:
                 sys.stdout.write(line)
-        sys.exit(-3)
+        sys.exit(-1)
 
     print('built %d test suites, %d test cases, %d permutations' % (
         len(suites),
         sum(len(suite.cases) for suite in suites),
         sum(len(suite.perms) for suite in suites)))
 
-    filtered = 0
+    total = 0
     for suite in suites:
         for perm in suite.perms:
-            filtered += perm.shouldtest(**args)
-    if filtered != sum(len(suite.perms) for suite in suites):
-        print('filtered down to %d permutations' % filtered)
+            total += perm.shouldtest(**args)
+    if total != sum(len(suite.perms) for suite in suites):
+        print('filtered down to %d permutations' % total)
 
     # only requested to build?
-    if args.get('build', False):
+    if args.get('build'):
         return 0
 
     print('====== testing ======')
@@ -697,15 +742,12 @@ def main(**args):
     failed = 0
     for suite in suites:
         for perm in suite.perms:
-            if not hasattr(perm, 'result'):
-                continue
-
             if perm.result == PASS:
                 passed += 1
-            else:
+            elif isinstance(perm.result, TestFailure):
                 sys.stdout.write(
                     "\033[01m{path}:{lineno}:\033[01;31mfailure:\033[m "
-                    "{perm} failed with {returncode}\n".format(
+                    "{perm} failed\n".format(
                         perm=perm, path=perm.suite.path, lineno=perm.lineno,
                         returncode=perm.result.returncode or 0))
                 if perm.result.stdout:
@@ -723,11 +765,33 @@ def main(**args):
                 sys.stdout.write('\n')
                 failed += 1
 
-    if args.get('gdb', False):
+    if args.get('coverage'):
+        # collect coverage info
+        # why -j1? lcov doesn't work in parallel because of gcov limitations
+        cmd = (['make', '-j1', '-f', 'Makefile'] +
+            list(it.chain.from_iterable(['-f', m] for m in makefiles)) +
+            (['COVERAGETARGET=%s' % args['coverage']]
+                if isinstance(args['coverage'], str) else []) +
+            [suite.path + '.info' for suite in suites
+                if any(perm.result == PASS for perm in suite.perms)])
+        if args.get('verbose'):
+            print(' '.join(shlex.quote(c) for c in cmd))
+        proc = sp.Popen(cmd,
+            stdout=sp.PIPE if not args.get('verbose') else None,
+            stderr=sp.STDOUT if not args.get('verbose') else None,
+            universal_newlines=True)
+        proc.wait()
+        if proc.returncode != 0:
+            if not args.get('verbose'):
+                for line in proc.stdout:
+                    sys.stdout.write(line)
+            sys.exit(-1)
+
+    if args.get('gdb'):
         failure = None
         for suite in suites:
             for perm in suite.perms:
-                if getattr(perm, 'result', PASS) != PASS:
+                if isinstance(perm.result, TestFailure):
                     failure = perm.result
         if failure is not None:
             print('======= gdb ======')
@@ -735,20 +799,22 @@ def main(**args):
             failure.case.test(failure=failure, **args)
             sys.exit(0)
 
-    print('tests passed: %d' % passed)
-    print('tests failed: %d' % failed)
+    print('tests passed %d/%d (%.2f%%)' % (passed, total,
+        100*(passed/total if total else 1.0)))
+    print('tests failed %d/%d (%.2f%%)' % (failed, total,
+        100*(failed/total if total else 1.0)))
     return 1 if failed > 0 else 0
 
 if __name__ == "__main__":
     import argparse
     parser = argparse.ArgumentParser(
         description="Run parameterized tests in various configurations.")
-    parser.add_argument('testpaths', nargs='*', default=[TESTDIR],
+    parser.add_argument('test_paths', nargs='*', default=[TEST_PATHS],
         help="Description of test(s) to run. By default, this is all tests \
             found in the \"{0}\" directory. Here, you can specify a different \
             directory of tests, a specific file, a suite by name, and even \
             specific test cases and permutations. For example \
-            \"test_dirs#1\" or \"{0}/test_dirs.toml#1#1\".".format(TESTDIR))
+            \"test_dirs#1\" or \"{0}/test_dirs.toml#1#1\".".format(TEST_PATHS))
     parser.add_argument('-D', action='append', default=[],
         help="Overriding parameter definitions.")
     parser.add_argument('-v', '--verbose', action='store_true',
@@ -769,10 +835,19 @@ if __name__ == "__main__":
         help="Run tests normally.")
     parser.add_argument('-r', '--reentrant', action='store_true',
         help="Run reentrant tests with simulated power-loss.")
-    parser.add_argument('-V', '--valgrind', action='store_true',
+    parser.add_argument('--valgrind', action='store_true',
         help="Run non-leaky tests under valgrind to check for memory leaks.")
-    parser.add_argument('-e', '--exec', default=[], type=lambda e: e.split(' '),
+    parser.add_argument('--exec', default=[], type=lambda e: e.split(),
         help="Run tests with another executable prefixed on the command line.")
-    parser.add_argument('-d', '--disk',
+    parser.add_argument('--disk',
         help="Specify a file to use for persistent/reentrant tests.")
+    parser.add_argument('--coverage', type=lambda x: x if x else True,
+        nargs='?', const='',
+        help="Collect coverage information during testing. This uses lcov/gcov \
+            to accumulate coverage information into *.info files. May also \
+            a path to a *.info file to accumulate coverage info into.")
+    parser.add_argument('--build-dir',
+        help="Build relative to the specified directory instead of the \
+            current directory.")
+
     sys.exit(main(**vars(parser.parse_args())))