From bed92fccdbfca24b731bb9b12972b8481ecce0b7 Mon Sep 17 00:00:00 2001 From: Nicholas Fraser Date: Thu, 25 Jul 2019 22:35:27 -0400 Subject: [PATCH] Converted unit test buildsystem from SCons to Lua+Ninja --- .gitignore | 3 +- .travis.yml | 34 +- README.md | 8 +- SConscript | 35 -- SConstruct | 239 -------------- {test => examples}/sax-example.c | 0 {test => examples}/sax-example.h | 0 test/README.md | 53 +++ test/fuzz-config.h | 2 + test/fuzz.c | 6 + test/mpack-config.h | 2 +- test/test-common.c | 2 +- tools/amalgamate.sh | 3 +- tools/ci.sh | 8 +- tools/gcov.sh | 4 +- tools/unittest.lua | 550 +++++++++++++++++++++++++++++++ 16 files changed, 642 insertions(+), 307 deletions(-) delete mode 100644 SConscript delete mode 100644 SConstruct rename {test => examples}/sax-example.c (100%) rename {test => examples}/sax-example.h (100%) create mode 100644 test/README.md create mode 100755 tools/unittest.lua diff --git a/.gitignore b/.gitignore index a209013..3807046 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ mpack-test-blank-file /analysis/ /docs/html/ /vgcore.* +/.ninja_deps +/.ninja_log # visual studio project *.suo @@ -32,4 +34,3 @@ xcuserdata/ # other junk .directory /tags - diff --git a/.travis.yml b/.travis.yml index c39c1bc..0b95474 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,14 @@ language: c -dist: trusty +dist: bionic sudo: false -addons: - apt: - packages: - - clang - - valgrind +# apparently apt addon doesn't work on bionic? +before_install: + - sudo apt-get update + - sudo apt-get install -y build-essential gcc-multilib g++-multilib clang cmake lua5.1 luarocks ninja-build libc6-dbg:i386 tcc valgrind + - CC= CXX= sudo luarocks install luafilesystem + - CC= CXX= sudo luarocks install rapidjson script: tools/ci.sh @@ -27,18 +28,19 @@ matrix: - os: osx compiler: clang env: "OSX=1" + # disable before_install here to avoid apt-get on mac + before_install: # code coverage build - os: linux compiler: gcov env: "STANDARD=1" - addons: - apt: - packages: - - valgrind - # these packages are needed for urllib3[secure] for SNI support for - # coveralls integration - - build-essential - - python-dev - - libffi-dev - - libssl-dev + before_install: + - sudo apt-get update + # these packages are needed for urllib3[secure] for SNI support for + # coveralls integration + - sudo apt-get install -y python-dev libffi-dev libssl-dev + # the rest of this is copied from above + - sudo apt-get install -y build-essential gcc-multilib g++-multilib clang cmake lua5.1 luarocks ninja-build libc6-dbg:i386 tcc valgrind + - CC= CXX= sudo luarocks install luafilesystem + - CC= CXX= sudo luarocks install rapidjson diff --git a/README.md b/README.md index 5f2f634..f23f482 100644 --- a/README.md +++ b/README.md @@ -133,12 +133,8 @@ The above issues greatly increase the complexity of the decoder. Full-featured J While the space inefficiencies of JSON can be partially mitigated through minification and compression, the performance inefficiencies cannot. More importantly, if you are minifying and compressing the data, then why use a human-readable format in the first place? -## Running the Unit Tests +## Testing MPack The MPack build process does not build MPack into a library; it is used to build and run the unit tests. You do not need to build MPack or the unit testing suite to use MPack. -On Linux, the test suite uses SCons and requires Valgrind, and can be run in the repository or in the amalgamation package. Run `scons` to build and run the test suite in full debug configuration. - -On Windows, there is a Visual Studio solution, and on OS X, there is an Xcode project for building and running the test suite. - -You can also build and run the test suite in all supported configurations, which is what the continuous integration server will build and run. If you are on 64-bit, you will need support for cross-compiling to 32-bit, and running 32-bit binaries with 64-bit Valgrind. On Ubuntu, you'll need `libc6-dbg:i386`. On Arch you'll need `gcc-multilib` or `lib32-clang`, and `valgrind-multilib`. Use `scons all=1 -j16` (or some appropriate thread count) to build and run all tests. +See [test/README.md](test/README.md) for information on how to test MPack. diff --git a/SConscript b/SConscript deleted file mode 100644 index 81fe929..0000000 --- a/SConscript +++ /dev/null @@ -1,35 +0,0 @@ -import platform - -Import('env', 'CPPFLAGS', 'LINKFLAGS', 'valgrind') - -# we add the C/C++ specific flags here. we can't use CCFLAGS/CXXFLAGS -# because as far as SCons is concerned, they are all C files; we're -# passing -x c++ to force the language. -if "c++" in CPPFLAGS: - CPPFLAGS += ["-Wmissing-declarations"] - LINKFLAGS += ["-lstdc++"] -else: - CPPFLAGS += ["-Wmissing-prototypes", "-Wc++-compat"] - -srcs = env.Object(env.Glob('src/mpack/*.c') + env.Glob('test/test*.c'), - CPPFLAGS=env['CPPFLAGS'] + CPPFLAGS) - -prog = env.Program("mpack-test", srcs, - LINKFLAGS=env['LINKFLAGS'] + LINKFLAGS) - -# only some architectures are supported by valgrind. we don't check -# whether it's available though because we want to force mpack developers -# to install and use it if their architecture supports it. - -if valgrind and platform.system() == "Linux" and platform.machine() in ["i386", "x86_64"]: - valgrind = "valgrind --leak-check=full --error-exitcode=1 " - valgrind += "--suppressions=tools/valgrind-suppressions " - # travis version of valgrind is too old, doesn't support leak kinds - if "TRAVIS" not in env["ENV"]: - valgrind += "--show-leak-kinds=all --errors-for-leak-kinds=all " -else: - valgrind = "" - -env.Default(env.AlwaysBuild(env.Alias("test", - [prog], - valgrind + Dir('.').path + "/mpack-test"))) diff --git a/SConstruct b/SConstruct deleted file mode 100644 index a8bb940..0000000 --- a/SConstruct +++ /dev/null @@ -1,239 +0,0 @@ -import platform, os - -simpletest = """ -int main(int argc, char** argv) { - // array dereference to test for the existence of - // sanitizer libs when using -fsanitize (libubsan) - return argv[argc - 1] == 0; -} -""" - -def CheckFlags(context, cppflags, linkflags = [], message = None, testcode = simpletest, testformat = '.c'): - if message == None: - message = " ".join(cppflags + ((cppflags != linkflags) and linkflags or [])) - context.Message("Checking for " + message + " support... ") - - env.Prepend(CPPFLAGS = cppflags, LINKFLAGS = linkflags) - result = context.TryLink(testcode, testformat) - env.Replace(CPPFLAGS = env["CPPFLAGS"][len(cppflags):], LINKFLAGS = env["LINKFLAGS"][len(linkflags):]) - - context.Result(result) - return result - -def AddFlagIfSupported(flag): - if conf.CheckFlags([flag]): - env.Append(CPPFLAGS = [flag]) - - -# Common environment setup - -env = Environment() -conf = Configure(env, custom_tests = {'CheckFlags': CheckFlags}) - -for x in os.environ.keys(): - if x in ["CC", "CXX"]: - env[x] = os.environ[x] - if x in ["PATH", "TRAVIS", "TERM"] or x.startswith("CLANG_") or x.startswith("CCC_"): - env["ENV"][x] = os.environ[x] - -env.Append(CPPFLAGS = [ - "-Wall", "-Wextra", "-Wpedantic", "-Werror", - "-Wconversion", "-Wundef", - "-Wshadow", "-Wcast-qual", - "-Isrc", "-Itest", - "-DMPACK_SCONS=1", - "-DMPACK_HAS_CONFIG=1", - "-g", - ]) -env.Append(LINKFLAGS = [ - "-g", - ]) - -# Check if isnanf is a function - -if conf.CheckFunc('isnanf'): - conf.env.Append(CPPFLAGS= '-DMPACK_ISNANF_IS_FUNC') - -# Additional warning flags are passed in SConscript based on the language (C/C++) - -AddFlagIfSupported("-Wmissing-variable-declarations") -AddFlagIfSupported("-Wstrict-aliasing=1") -AddFlagIfSupported("-Wfloat-conversion") -AddFlagIfSupported("-Wmisleading-indentation") - - -# Optional flags used in various builds - -defaultfeatures = [ - "-DMPACK_READER=1", - "-DMPACK_WRITER=1", - "-DMPACK_EXPECT=1", - "-DMPACK_NODE=1", -] -allfeatures = defaultfeatures + [ - "-DMPACK_COMPATIBILITY=1", - "-DMPACK_EXTENSIONS=1", -] - -noioconfigs = [ - "-DMPACK_STDLIB=1", - "-DMPACK_MALLOC=test_malloc", - "-DMPACK_FREE=test_free", -] -allconfigs = noioconfigs + ["-DMPACK_STDIO=1"] - -hasOg = conf.CheckFlags(["-Og"]) -if hasOg: - debugflags = ["-DDEBUG", "-Og"] -else: - debugflags = ["-DDEBUG", "-O0"] -releaseflags = ["-O2"] - -hasC11 = conf.CheckFlags(["-std=c11"]) -if hasC11: - cflags = ["-std=c11"] -else: - cflags = ["-std=c99"] - -gcovflags = [] -if ARGUMENTS.get('gcov'): - gcovflags = [ - "-DMPACK_GCOV=1", - "--coverage", - "-fno-inline", - "-fno-inline-small-functions", - "-fno-default-inline" - ] - -ltoflags = ["-O3", "-flto", "-fuse-linker-plugin", "-fno-fat-lto-objects"] - -if conf.CheckFlags(["-Wstrict-aliasing=3"]): - ltoflags.append("-Wstrict-aliasing=3") -elif conf.CheckFlags(["-Wstrict-aliasing=2"]): - ltoflags.append("-Wstrict-aliasing=2") - - -# -lstdc++ is added in SConscript -cxxflags = ["-x", "c++"] - - -# Functions to add a variant build. One variant build will build and run the -# entire library and test suite in a given configuration. - -def AddBuild(variant_dir, cppflags, linkflags = [], valgrind = True): - env.SConscript("SConscript", - variant_dir="build/" + variant_dir, - src="../..", - exports={ - 'env': env, - 'CPPFLAGS': cppflags, - 'LINKFLAGS': linkflags, - 'valgrind': valgrind, - }, - duplicate=0) - -def AddBuilds(variant_dir, cppflags, linkflags = [], valgrind = True): - AddBuild("debug-" + variant_dir, debugflags + cppflags, debugflags + linkflags, valgrind) - if ARGUMENTS.get('all'): - AddBuild("release-" + variant_dir, releaseflags + cppflags, releaseflags + linkflags, valgrind) - - -# The default build, everything in debug. This is also the build -# used for code coverage measurement and static analysis. -# Note that the default build does not use the default config; it enables -# MPACK_COMPATIBILITY and MPACK_EXTENSIONS. -AddBuild("debug", defaultfeatures + allconfigs + debugflags + cflags + gcovflags, gcovflags) - - -# Run "scons more=1" to run a handful of builds that are likely -# to reveal configuration errors. -if ARGUMENTS.get('more') or ARGUMENTS.get('all'): - AddBuild("release", allfeatures + allconfigs + releaseflags + cflags) - AddBuilds("default", defaultfeatures + allconfigs + cflags) - AddBuilds("embed", defaultfeatures + cflags + ["-DMPACK_NO_BUILTINS=1"]) - AddBuilds("noio", allfeatures + noioconfigs + cflags) - AddBuild("debug-size", ["-DMPACK_OPTIMIZE_FOR_SIZE=1"] + debugflags + allfeatures + allconfigs + cflags) - if conf.CheckFlags(cxxflags + ["-std=c++11"], [], "-std=c++11"): - AddBuilds("cxx11", allfeatures + allconfigs + cxxflags + ["-std=c++11"]) - - -# Run "scons all=1" to run all builds. This is what the CI runs. -if ARGUMENTS.get('all'): - - # various release builds - AddBuild("release-unopt", allfeatures + allconfigs + cflags + ["-O0"]) - AddBuild("release-fastmath", allfeatures + allconfigs + releaseflags + cflags + ["-ffast-math"]) - if conf.CheckFlags(ltoflags, ltoflags, "-flto"): - AddBuild("release-lto", allfeatures + allconfigs + ltoflags + cflags, ltoflags) - AddBuild("release-size", ["-Os", "-DMPACK_STRINGS=0"] + allfeatures + allconfigs + cflags) - - # feature subsets with default configuration - AddBuilds("empty", allconfigs + cflags) - AddBuilds("writer", ["-DMPACK_WRITER=1"] + allconfigs + cflags) - AddBuilds("reader", ["-DMPACK_READER=1"] + allconfigs + cflags) - AddBuilds("expect", ["-DMPACK_READER=1", "-DMPACK_EXPECT=1"] + allconfigs + cflags) - AddBuilds("node", ["-DMPACK_NODE=1"] + allconfigs + cflags) - AddBuilds("compatibility", ["-DMPACK_COMPATIBILITY=1"] + defaultfeatures + allconfigs + cflags) - AddBuilds("extensions", ["-DMPACK_EXTENSIONS=1"] + defaultfeatures + allconfigs + cflags) - - # no i/o - AddBuilds("noio-writer", ["-DMPACK_WRITER=1"] + noioconfigs + cflags) - AddBuilds("noio-reader", ["-DMPACK_READER=1"] + noioconfigs + cflags) - AddBuilds("noio-expect", ["-DMPACK_READER=1", "-DMPACK_EXPECT=1"] + noioconfigs + cflags) - AddBuilds("noio-node", ["-DMPACK_NODE=1"] + noioconfigs + cflags) - - # embedded builds without libc (using builtins) - AddBuilds("embed-writer", ["-DMPACK_WRITER=1"] + cflags) - AddBuilds("embed-reader", ["-DMPACK_READER=1"] + cflags) - AddBuilds("embed-expect", ["-DMPACK_READER=1", "-DMPACK_EXPECT=1"] + cflags) - AddBuilds("embed-node", ["-DMPACK_NODE=1"] + cflags) - AddBuilds("embed-full", allfeatures + cflags) - - # miscellaneous test builds - AddBuilds("notrack", ["-DMPACK_NO_TRACKING=1"] + allfeatures + allconfigs + cflags) - AddBuilds("realloc", allfeatures + allconfigs + debugflags + cflags + ["-DMPACK_REALLOC=test_realloc"]) - if hasOg: - AddBuild("debug-O0", allfeatures + allconfigs + ["-DDEBUG", "-O0"] + cflags) - - # other language standards (C99, various C++ versions) - # Note: We disable pedantic in C++98 due to our use of variadic macros, - # trailing commas, ll format specifiers, and probably more. We technically - # only support C++98 with those extensions. - AddBuilds("cxx", allfeatures + allconfigs + cxxflags + ["-std=c++98", "-Wno-pedantic"]) - if hasC11: - AddBuilds("c99", allfeatures + allconfigs + ["-std=c99"]) - if conf.CheckFlags(cxxflags + ["-std=c++11"], [], "-std=c++11"): - AddBuilds("cxx11", allfeatures + allconfigs + cxxflags + ["-std=c++11"]) - # Make sure C++ compiles with disabled features (see #66) - AddBuilds("cxx11-empty", allconfigs + cxxflags + ["-std=c++11"]) - if conf.CheckFlags(cxxflags + ["-std=c++14"], [], "-std=c++14"): - AddBuilds("cxx14", allfeatures + allconfigs + cxxflags + ["-std=c++14"]) - if conf.CheckFlags(cxxflags + ["-std=gnu++11"], [], "-std=gnu++11"): - AddBuilds("gnuxx11", allfeatures + allconfigs + cxxflags + ["-std=gnu++11"]) # Clang supports _Generic in gnu++11 mode - - # 32-bit builds - if conf.CheckFlags(["-m32"], ["-m32"]): - AddBuilds("32bit", allfeatures + allconfigs + cflags + ["-m32"], ["-m32"]) - # As above, pedantic is disabled in C++98 - AddBuilds("cxx-32bit", allfeatures + allconfigs + cxxflags + ["-std=c++98", "-Wno-pedantic", "-m32"], ["-m32"]) - if conf.CheckFlags(cxxflags + ["-std=c++11", "-m32"], ["-m32"], "-std=c++11"): - AddBuilds("cxx11-32bit", allfeatures + allconfigs + cxxflags + ["-std=c++11", "-m32"], ["-m32"]) - - # sanitize build tests - sanitizers = { - "stack-protector": ["-Wstack-protector", "-fstack-protector-all"], - "undefined": ["-fsanitize=undefined"], - # ASAN is temporarily disabled because the containerized Travis-CI - # nodes no longer allow ptrace. We need to switch to our own docker - # image anyway to get newer compilers so we'll add the ptrace cap to it - # and re-enable this. - # https://github.com/google/sanitizers/issues/764 - #"address": ["-fsanitize=address"], - "safestack": ["-fsanitize=safe-stack"], - } - # memory sanitizer isn't working on the version of Clang on Travis-CI's Trusty container right now - if not ("CC" in os.environ and os.environ["CC"] == "clang" and "TRAVIS" in os.environ): - sanitizers["memory"] = ["-fsanitize=memory"] - for name, flags in sanitizers.items(): - if conf.CheckFlags(flags, flags): - AddBuilds("sanitize-" + name, allfeatures + allconfigs + cflags + flags, flags, valgrind=False) diff --git a/test/sax-example.c b/examples/sax-example.c similarity index 100% rename from test/sax-example.c rename to examples/sax-example.c diff --git a/test/sax-example.h b/examples/sax-example.h similarity index 100% rename from test/sax-example.h rename to examples/sax-example.h diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..3738325 --- /dev/null +++ b/test/README.md @@ -0,0 +1,53 @@ +The MPack build process does not build MPack into a library; it is used to build and run various tests. You do not need to build MPack or the unit testing suite to use MPack. + +# Unit Tests + +## Linux + +### Dependencies + +The unit test suite on Linux has the following requirements: + +- Lua, at least version 5.1, with the following rocks: + - luafilesystem + - rapidjson (this requires CMake to build) +- Ninja +- 32-bit cross-compiling support, if on a 64-bit system + - On Arch, this is `gcc-multilib` or `lib32-clang` + - On Ubuntu, this is `libc6-dbg:i386` +- Valgrind + - Including debugging 32-bit apps with 64-bit Valgrind, e.g. `valgrind-multilib` + +For example, on Ubuntu: + +```sh +# This installs Lua 5.1. Newer versions of Ubuntu may use a newer version. +sudo apt install build-essential cmake lua5.1 luarocks ninja-build libc6-dbg:i386 valgrind +``` + +Or on Arch: + +```sh +sudo pacman -S --needed gcc-multilib cmake lua luarocks ninja valgrind +``` + +Then: + +```sh +sudo luarocks install luafilesystem +sudo luarocks install rapidjson +``` + +### Running the tests + +Run the default tests with: `tools/unittest.lua` + +You can run additional tests by passing specific targets on the command-line. The "more" or "all" targets can run additional tests, and the "help" target lists tests. The CI runs "all" under various compilers. + +## Other platforms + +On Windows, there is a Visual Studio solution, and on OS X, there is an Xcode project for building and running the test suite. + +# Fuzz Testing + +MPack supports fuzzing with american fuzzy lop. Run `tools/afl.sh` to fuzz MPack. diff --git a/test/fuzz-config.h b/test/fuzz-config.h index 5d4ac46..9a1dc32 100644 --- a/test/fuzz-config.h +++ b/test/fuzz-config.h @@ -1,6 +1,8 @@ #ifndef MPACK_FUZZ_CONFIG_H #define MPACK_FUZZ_CONFIG_H +#define MPACK_FUZZ + // we use small buffer sizes to test flushing and growing #define MPACK_TRACKING_INITIAL_CAPACITY 3 #define MPACK_STACK_SIZE 33 diff --git a/test/fuzz.c b/test/fuzz.c index 6c885ad..b9a90d2 100644 --- a/test/fuzz.c +++ b/test/fuzz.c @@ -19,6 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#ifdef MPACK_FUZZ + /* * fuzz.c is a test program to assist with fuzzing MPack. It: * @@ -162,3 +164,7 @@ int main(int argc, char** argv) { return EXIT_SUCCESS; } + +#else +typedef int mpack_pedantic_allow_empty_translation_unit; +#endif diff --git a/test/mpack-config.h b/test/mpack-config.h index 64a75a4..073aafd 100644 --- a/test/mpack-config.h +++ b/test/mpack-config.h @@ -10,7 +10,7 @@ #define MPACK_DEBUG 1 #endif -#ifdef MPACK_SCONS +#ifdef MPACK_VARIANT_BUILDS // Most options such as featureset and platform configuration // are specified by the SCons buildsystem. Any options that are // unset on the command line are considered disabled. diff --git a/test/test-common.c b/test/test-common.c index 73531a9..0f8cf4e 100644 --- a/test/test-common.c +++ b/test/test-common.c @@ -287,7 +287,7 @@ static void test_strings() { // test strings for invalid enum values // (invalid enum values cause undefined behavior in C++) - #if MPACK_DEBUG && !defined(__cplusplus) + #if MPACK_DEBUG && !defined(__cplusplus) && MPACK_STRINGS TEST_ASSERT(mpack_error_to_string((mpack_error_t)-1)); TEST_ASSERT(mpack_type_to_string((mpack_type_t)-1)); #endif diff --git a/tools/amalgamate.sh b/tools/amalgamate.sh index 71e4515..ffc2e6e 100755 --- a/tools/amalgamate.sh +++ b/tools/amalgamate.sh @@ -29,13 +29,12 @@ TOOLS="\ tools/clean.sh \ tools/gcov.sh \ tools/scan-build.sh \ + tools/unittest.lua \ tools/valgrind-suppressions \ " FILES="\ test \ - SConscript \ - SConstruct \ LICENSE \ AUTHORS.md \ README.md \ diff --git a/tools/ci.sh b/tools/ci.sh index 0bad517..1f279e4 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -30,18 +30,18 @@ pwd if [[ "$CC" == "scan-build" ]]; then unset CC unset CXX - scan-build -o analysis --use-cc=`which clang` --status-bugs scons + scan-build -o analysis --use-cc=`which clang` --status-bugs tools/unittest.lua elif [[ "$CC" == "gcov" ]]; then unset CC unset CXX - scons gcov=1 + tools/unittest.lua run-coverage tools/gcov.sh - pip install --user idna==2.5 # below packages conflict with idna-2.6 + pip install --user idna==2.5 # below packages conflict with idna-2.6 (not sure if this is still necessary on bionic) pip install --user cpp-coveralls urllib3[secure] coveralls --no-gcov --include src else - scons all=1 + tools/unittest.lua all fi diff --git a/tools/gcov.sh b/tools/gcov.sh index 9b635bf..ebc0702 100755 --- a/tools/gcov.sh +++ b/tools/gcov.sh @@ -1,3 +1,3 @@ #!/bin/sh -# tests must be built with "scons gcov=1" -gcov --object-directory build/debug/src/mpack `find src -name '*.c'` || exit $? +# tests must be run with "test/build.lua run-coverage". it's not included in "all". +gcov --object-directory build/coverage/objs/src/mpack `find src -name '*.c'` || exit $? diff --git a/tools/unittest.lua b/tools/unittest.lua new file mode 100755 index 0000000..206899e --- /dev/null +++ b/tools/unittest.lua @@ -0,0 +1,550 @@ +#!/usr/bin/env lua +-- +-- Copyright (c) 2015-2019 Nicholas Fraser +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-- + +--------------------------------------------------- + +-- This is the buildsystem for the MPack unit test suite. It tests the compiler +-- for support for various flags and features, generates a ninja build file, +-- and runs ninja on it with the targets given on the command line. +-- +-- This requires at least Lua 5.1, with rocks luafilesystem and rapidjson. + +require 'lfs' +local rapidjson = require 'rapidjson' + +-- Returns a new array containing the values of all given arrays in order +function concatArrays(...) + local z = {} + for _, t in ipairs({...}) do + if type(t) ~= "table" then + assert(false) + end + for _, value in ipairs(t) do + table.insert(z, value) + end + end + return z +end + +-- Runs the given command, returning true if successful +function execute(args) + local ret = os.execute(args) + -- os.execute() return types changed between lua 5.1 and 5.3 + return type(ret) == "boolean" and ret or ret == 0 +end + +--------------------------------------------------- +-- Load Cached Config +--------------------------------------------------- + +local ccvar = os.getenv("CC") +local config = {ccvar = os.getenv("CC")} + +-- try to load cached config +local config_filename = "build/config/config.json" +local configfile = io.open(config_filename, "rb") +if configfile ~= nil then + local configdata = configfile:read("*all") + configfile:close() + local loadedconfig = rapidjson.decode(configdata) + if loadedconfig ~= nil and loadedconfig.ccvar == ccvar then + print("Loaded cached configuration.") + config = loadedconfig + end +end + +if config.flags == nil then + config.flags = {} +end + +--------------------------------------------------- +-- Environment +--------------------------------------------------- + +if config.cc == nil then + if ccvar ~= nil then + config.cc = ccvar + else + -- prefer gcc. better warnings, faster code, slower compilation + local ret = execute(table.concat({"which", "gcc", "1>/dev/null", "2>/dev/null"}, ' ')) + if ret then + print("GCC found.") + config.cc = "gcc" + else + print("GCC not found.") + config.cc = "cc" + end + end + print("Compiler: " .. config.cc) +end +local cc = config.cc + +lfs.mkdir("build") +lfs.mkdir("build/config") + +--------------------------------------------------- +-- Test Flags +--------------------------------------------------- + +local out = io.open("build/config/flagtest.c", "wb") +out:write([[ +int main(int argc, char** argv) { + // array dereference to test for the existence of + // sanitizer libs when using -fsanitize (libubsan) + return argv[argc - 1] == 0; +} +]]) +out:close() + +-- Check whether the given flag is (or flags are) supported. If it's not in +-- cached config, it's tested and stored. +function checkFlag(flag) + if config.flags[flag] ~= nil then + -- io.write("Cached flag \"",flag,"\" ", config.flags[flag] and "supported" or "not supported.", "\n") + return config.flags[flag] + end + io.write("Testing flag(s): " .. flag .. " ... ") + io.flush() + --io.write(table.concat({cc, flag, "build/config/flagtest.c", "-o", "build/config/flagtest", "1>&2"}, ' ')) + local ret = execute(table.concat({cc, "-Werror", flag, "build/config/flagtest.c", "-o", "build/config/flagtest", "2>/dev/null"}, ' ')) + if ret then + print("Supported.") + config.flags[flag] = true + else + print("Not supported.") + config.flags[flag] = false + end + return config.flags[flag] +end + +-- extra warnings. if we have them, we enable them in all builds. +extra_warnings_to_test = { + "-Wpedantic", + "-Wmissing-variable-declarations", + "-Wfloat-conversion", + "-fstrict-aliasing", +} + +local extra_warnings = {} +config.extra_warnings = extra_warnings + +for _, flag in ipairs(extra_warnings_to_test) do + if checkFlag(flag) then + table.insert(extra_warnings, flag) + end +end + +if checkFlag("-Wstrict-aliasing=3") then + table.insert(extra_warnings, "-Wstrict-aliasing=3") +elseif checkFlag("-Wstrict-aliasing=2") then + table.insert(extra_warnings, "-Wstrict-aliasing=2") +elseif checkFlag("-Wstrict-aliasing") then + table.insert(extra_warnings, "-Wstrict-aliasing") +end + +-- We use -Og for all debug builds if we have it, but ONLY under GCC. It can +-- sometimes improve warnings, and things run a lot faster especially under +-- Valgrind, but Clang stupidly maps it to -O1 which has some optimizations +-- that break debugging! +-- We don't bother checking if "cc" is actually "gcc" or any other +-- nonsense. We just lazily check if "gcc" is in the compiler name. +local hasOg = cc:find("gcc") and checkFlag("-Og") + +isnanf_test = [[ +#include +int main(void) { + return isnanf(5.5f); +} +]] + +if config.isnanf == nil then + io.write("Testing isnanf ... ") + io.flush() + local out = io.open("build/nantest.c", "wb") + out:write(isnanf_test) + out:close() + local ret = execute(table.concat({cc, "build/nantest.c", "-o", "build/nantest", "2>/dev/null"}, ' ')) + if ret then + print("Supported.") + config.isnanf = true + else + print("Not supported.") + config.isnanf = false + end +end +if config.isnanf then + table.insert(extra_warnings, '-DMPACK_ISNANF_IS_FUNC') +end + + +--------------------------------------------------- +-- Other Flags +--------------------------------------------------- + +-- TODO: MPACK_HAS_CONFIG is off by default, so we should test without it. Unit +-- test configuration can be done with a force include file. +global_cppflags = concatArrays({ + "-Wall", "-Wextra", "-Werror", + "-Wconversion", "-Wundef", + "-Wshadow", "-Wcast-qual", + "-Isrc", "-Itest", + "-DMPACK_VARIANT_BUILDS=1", + "-DMPACK_HAS_CONFIG=1", + "-g", +}, extra_warnings) + +-- if cc == "clang" then +-- global_cppflags = concatArrays(global_cppflags, {}) +-- elseif cc == "gcc" then +-- "-Wmisleading-indentation", +-- end +-- global_cppflags = concatArrays(global_cppflags, {"-Wmissing-declarations"}) + +defaultfeatures = { + "-DMPACK_READER=1", + "-DMPACK_WRITER=1", + "-DMPACK_EXPECT=1", + "-DMPACK_NODE=1", +} + +allfeatures = concatArrays(defaultfeatures, { + "-DMPACK_COMPATIBILITY=1", + "-DMPACK_EXTENSIONS=1", +}) + +noioconfigs = { + "-DMPACK_STDLIB=1", + "-DMPACK_MALLOC=test_malloc", + "-DMPACK_FREE=test_free", +} + +allconfigs = concatArrays({}, noioconfigs, { + "-DMPACK_STDIO=1", +}) + +debugflags = { + "-DDEBUG", hasOg and "-Og" or "-O0", +} + +releaseflags = { + "-O2", +} + +cflags = { + checkFlag("-std=c11") and "-std=c11" or "-std=c99", + "-Wc++-compat" +} +if checkFlag("-Wmissing-prototypes") then + cflags = concatArrays(cflags, {"-Wmissing-prototypes"}) +end + +cxxflags = { + "-x", "c++", + "-Wmissing-declarations", +} + +--------------------------------------------------- +-- Build configurations +--------------------------------------------------- + +builds = {} + +function addBuild(name, cppflags, ldflags) + builds[name] = { + cppflags = cppflags, + ldflags = ldflags + } +end + +function addDebugReleaseBuilds(name, cppflags, ldflags) + addBuild(name .. "-debug", concatArrays(cppflags, debugflags), ldflags) + addBuild(name .. "-release", concatArrays(cppflags, releaseflags), ldflags) +end + +addDebugReleaseBuilds('default', concatArrays(defaultfeatures, allconfigs, cflags)); +addDebugReleaseBuilds('everything', concatArrays(allfeatures, allconfigs, cflags)); +builds["everything-debug"].run_wrapper = "valgrind" +builds["everything-release"].run_wrapper = "valgrind" +addDebugReleaseBuilds('empty', concatArrays(allconfigs, cflags)); +addDebugReleaseBuilds('writer', concatArrays({"-DMPACK_WRITER=1"}, allconfigs, cflags)); +addDebugReleaseBuilds('reader', concatArrays({"-DMPACK_READER=1"}, allconfigs, cflags)); +addDebugReleaseBuilds('expect', concatArrays({"-DMPACK_READER=1", "-DMPACK_EXPECT=1"}, allconfigs, cflags)); +addDebugReleaseBuilds('node', concatArrays({"-DMPACK_NODE=1"}, allconfigs, cflags)); +addDebugReleaseBuilds('compatibility', concatArrays(defaultfeatures, {"-DMPACK_COMPATIBILITY=1"}, allconfigs, cflags)); +addDebugReleaseBuilds('extensions', concatArrays(defaultfeatures, {"-DMPACK_EXTENSIONS=1"}, allconfigs, cflags)); + +-- no i/o +addDebugReleaseBuilds('noio', concatArrays(allfeatures, noioconfigs, cflags)); +addDebugReleaseBuilds('noio-writer', concatArrays({"-DMPACK_WRITER=1"}, noioconfigs, cflags)); +addDebugReleaseBuilds('noio-reader', concatArrays({"-DMPACK_READER=1"}, noioconfigs, cflags)); +addDebugReleaseBuilds('noio-expect', concatArrays({"-DMPACK_READER=1", "-DMPACK_EXPECT=1"}, noioconfigs, cflags)); +addDebugReleaseBuilds('noio-node', concatArrays({"-DMPACK_NODE=1"}, noioconfigs, cflags)); + +-- embedded builds without libc (using builtins) +addDebugReleaseBuilds('embed', concatArrays(allfeatures, cflags)); +addDebugReleaseBuilds('embed-writer', concatArrays({"-DMPACK_WRITER=1"}, cflags)); +addDebugReleaseBuilds('embed-reader', concatArrays({"-DMPACK_READER=1"}, cflags)); +addDebugReleaseBuilds('embed-expect', concatArrays({"-DMPACK_READER=1", "-DMPACK_EXPECT=1"}, cflags)); +addDebugReleaseBuilds('embed-node', concatArrays({"-DMPACK_NODE=1"}, cflags)); +addDebugReleaseBuilds('embed-nobuiltins', concatArrays({"-DMPACK_NO_BUILTINS=1"}, allfeatures, cflags)); + +-- language versions +if checkFlag("-std=c11") then + -- if we're using c11 for everything else, we still need to test c99 + addDebugReleaseBuilds('c99', concatArrays(allfeatures, allconfigs, {"-std=c99"})); +end +for _, version in ipairs({"c++11", "gnu++11", "c++14", "c++17"}) do + local flags = concatArrays(cxxflags, {"-std=" .. version}) + if checkFlag(table.concat(flags, ' ')) then + addDebugReleaseBuilds(version, concatArrays(allfeatures, allconfigs, flags)); + end +end + +-- Make sure C++11 compiles with disabled features (see #66) +local cxx11flags = concatArrays(cxxflags, {"-std=c++11"}) +if checkFlag(table.concat(cxx11flags, ' ')) then + addDebugReleaseBuilds('c++11-empty', concatArrays(allconfigs, cxx11flags)); +end + +-- We disable pedantic in C++98 due to our use of variadic macros, trailing +-- commas, ll format specifiers, and probably more. We technically only support +-- C++98 with those extensions. +cxx98flags = concatArrays(cxxflags, {"-std=c++98"}) +if checkFlag("-Wno-pedantic") then + table.insert(cxx98flags, "-Wno-pedantic") +end +addDebugReleaseBuilds('c++98', concatArrays(allfeatures, allconfigs, cxx98flags)) + +-- 32-bit builds +if checkFlag("-m32") then + addDebugReleaseBuilds('m32', concatArrays(allfeatures, allconfigs, cflags, {"-m32"}), {"-m32"}); + addDebugReleaseBuilds('cxx98-m32', concatArrays(allfeatures, allconfigs, cxx98flags, {"-m32"}), {"-m32"}); + if checkFlag(table.concat(cxx11flags, ' ')) then + addDebugReleaseBuilds('c++11-m32', concatArrays(allfeatures, allconfigs, cxx11flags, {"-m32"}), {"-m32"}); + end +end + +-- lto buld +local ltoflags = nil +if checkFlag("-O3 -flto -fuse-linker-plugin -fno-fat-lto-objects") then + ltoflags = concatArrays(allfeatures, allconfigs, cflags, + {"-O3", "-flto", "-fuse-linker-plugin", "-fno-fat-lto-objects"}) +elseif checkFlag("-O3 -flto") then + ltoflags = concatArrays(allfeatures, allconfigs, cflags, {"-O3", "-flto"}) +end +if ltoflags then + addBuild('lto', ltoflags, ltoflags) + builds["lto"].run_wrapper = "valgrind" +end + +-- miscellaneous special builds +addBuild('O3', concatArrays(allfeatures, allconfigs, cflags, {"-O3"})) +builds["O3"].run_wrapper = "valgrind" +addBuild('fastmath', concatArrays(allfeatures, allconfigs, cflags, {"-ffast-math"})) +builds["fastmath"].run_wrapper = "valgrind" +addDebugReleaseBuilds('optimize-size', concatArrays(allfeatures, allconfigs, cflags, + {"-DMPACK_OPTIMIZE_FOR_SIZE=1 -DMPACK_STRINGS=0"})); +addBuild('coverage', concatArrays(allfeatures, allconfigs, cflags, + {"-DMPACK_GCOV=1", "--coverage", "-fno-inline", "-fno-inline-small-functions", "-fno-default-inline"}), + {"--coverage"}) +addBuild('notrack', concatArrays(allfeatures, allconfigs, cflags, debugflags, {"-DMPACK_NO_TRACKING=1"})) +addDebugReleaseBuilds('realloc', concatArrays(allfeatures, allconfigs, cflags, {"-DMPACK_REALLOC=test_realloc"})) +builds["fastmath"].run_wrapper = "valgrind" +builds["coverage"].exclude = true -- don't run during "all". run separately by travis. +if hasOg then + addBuild('O0', concatArrays(allfeatures, allconfigs, cflags, debugflags, {"-DDEBUG", "-O0"})) +end + +-- sanitizers +function addSanitizerBuilds(name, cppflags, ldflags) + if checkFlag(table.concat(cppflags, " ")) then + addDebugReleaseBuilds(name, concatArrays(allfeatures, allconfigs, cflags, flags), ldflags) + end +end +addSanitizerBuilds('sanitize-stack-protector', {"-Wstack-protector", "-fstack-protector-all"}) +addSanitizerBuilds('sanitize-undefined', {"-fsanitize=undefined"}) +addSanitizerBuilds('sanitize-safe-stack', {"-fsanitize=safe-stack"}) +-- ASAN is temporarily disabled because the containerized Travis-CI +-- nodes no longer allow ptrace. We need to switch to our own docker +-- image anyway to get newer compilers so we'll add the ptrace cap to it +-- and re-enable this. +-- https://github.com/google/sanitizers/issues/764 +-- memory sanitizer isn't working on the version of Clang on Travis-CI's Trusty +-- container right now either +if not os.getenv("TRAVIS") then + addSanitizerBuilds('sanitize-address', {"-fsanitize=address"}, {"-fsanitize=address"}) + addSanitizerBuilds('sanitize-memory', {"-fsanitize=memory"}) +end + +sortedBuilds = {} +for build, _ in pairs(builds) do + table.insert(sortedBuilds, build) +end +table.sort(sortedBuilds) + +--------------------------------------------------- +-- Ninja generation +--------------------------------------------------- + +srcs = {} + +for _, dir in ipairs({"src/mpack", "test"}) do + for file in lfs.dir(dir) do + if #file > 2 and string.sub(file, -2) == ".c" then + table.insert(srcs, dir .. "/" .. file) + end + end +end + +local out = io.open("build/build.ninja", "wb") + +out:write("# This file is auto-generated.\n") +out:write("# Do not edit it; your changes will be erased.\n") +out:write("\n") + +-- 1.3 for gcc deps, 1.1 for pool +out:write("ninja_required_version = 1.3\n") +out:write("\n") + +out:write("rule compile\n") +out:write(" command = ", cc, " ", checkFlag("-MMD") and "-MMD" or "-MD", " -MF $out.d $flags -c $in -o $out\n") +out:write(" deps = gcc\n") +out:write(" depfile = $out.d\n") +out:write("\n") + +out:write("rule link\n") +out:write(" command = ", cc, " $flags $in -o $out\n") +out:write("\n") + +-- unfortunately right now the unit tests all try to write to the same files, +-- so they break when run concurrently. we need to make it write to files under +-- that config's build/ folder; for now we just run them sequentially. +out:write("pool run_pool\n") +out:write(" depth = 1\n") +out:write("run_wrapper =\n") +out:write("rule run\n") +out:write(" command = $run_wrapper $in\n") +out:write(" pool = run_pool\n") +out:write("\n") + +out:write("rule clean\n") +out:write(" command = rm -rf build .ninja_deps .ninja_log\n") +out:write("build clean: clean\n") +out:write("\n") + +out:write("rule help\n") +out:write(" command = cat build/help\n") +out:write("build help: help\n") +out:write("\n") + +for _, build in ipairs(sortedBuilds) do + local flags = builds[build] + local buildfolder = "build/" .. build + local cppflags = concatArrays(global_cppflags, flags.cppflags) + local ldflags = flags.ldflags or {} + local objs = {} + + for _, src in ipairs(srcs) do + local obj = buildfolder .. "/objs/" .. string.sub(src, 0, -3) .. ".o" + table.insert(objs, obj) + out:write("build ", obj, ": compile ", src, "\n") + out:write(" flags = ", table.concat(cppflags, " "), "\n") + end + + out:write("build ", buildfolder, "/runner: link ", table.concat(objs, " "), "\n") + out:write(" flags = ", table.concat(ldflags, " "), "\n") + + -- You can omit "run-" in front of any build to just build it without + -- running it. This lets you run it some other way (e.g. under gdb, + -- with/without Valgrind, etc.) + out:write("build ", build, ": phony ", buildfolder, "/runner\n\n") + + out:write("build run-", build, ": run ", buildfolder, "/runner\n") + if builds[build].run_wrapper then + out:write(" run_wrapper = ", builds[build].run_wrapper) + if builds[build].run_wrapper == "valgrind" then + out:write(" --leak-check=full --error-exitcode=1") + out:write(" --suppressions=tools/valgrind-suppressions") + if not os.getenv("TRAVIS") then + out:write(" --show-leak-kinds=all --errors-for-leak-kinds=all") + end + end + end + out:write("\n") +end + +out:write("default run-everything-debug\n") +out:write("build default: phony run-everything-debug\n") +out:write("\n") + +out:write("build more: phony run-everything-debug run-everything-release run-default-debug run-embed-debug run-embed-release") +if ltoflags then + out:write(" run-lto") +end +out:write("\n\n") + +out:write("build all: phony") +for _, build in ipairs(sortedBuilds) do + if not builds[build].exclude then + out:write(" run-") + out:write(build) + end +end +out:write("\n") + +out:close() + +local out = io.open("build/help", "wb") +out:write("\n") +out:write("Available targets:\n") +out:write("\n") +out:write(" (default)\n") +out:write(" more\n") +out:write(" all\n") +out:write(" clean\n") +out:write(" help\n") +out:write("\n") +for _, build in ipairs(sortedBuilds) do + out:write(" run-" .. build .. "\n") +end +out:close() + +--------------------------------------------------- +-- Write cached config +--------------------------------------------------- + +-- done configuring. cache our config for later. +rapidjson.dump(config, config_filename, {pretty=true}) + +--------------------------------------------------- +-- Ninja execution +--------------------------------------------------- + +local ninja = {"ninja", "-f", "build/build.ninja"} +for _, target in ipairs(arg) do + table.insert(ninja, target) +end + +local ret = execute(table.concat(ninja, ' ')) +if not ret then + os.exit(1) +end