diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22a7021..28fe1e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,28 @@ jobs: - name: Run Tests run: ./scripts/run_tests.sh + build-linux-ubsan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: sudo apt-get update && sudo apt-get install -y clang + - name: Build tests + run: CXX=clang++ STDVERSION=c++17 ARCH=-m64 USE_UBSAN=1 ./scripts/compile.sh + - name: Run Tests + run: ./scripts/run_tests.sh + + build-linux-msan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: sudo apt-get update && sudo apt-get install -y clang + - name: Build tests + run: CXX=clang++ STDVERSION=c++17 ARCH=-m64 USE_MSAN=1 ./scripts/compile.sh + - name: Run Tests + run: ./scripts/run_tests.sh + build-macos: strategy: matrix: @@ -33,6 +55,26 @@ jobs: - name: Run Tests run: ./scripts/run_tests.sh + build-linux-staticanalyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: sudo apt-get update && sudo apt-get install -y clang + - name: Build tests + run: CXX=clang++ STDVERSION=c++17 ARCH=-m64 USE_STATICANALYZE=1 ./scripts/compile.sh + - name: Run Tests + run: ./scripts/run_tests.sh + + build-macos-staticanalyze: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Build tests + run: CXX=clang++ STDVERSION=c++17 ARCH=-m64 USE_STATICANALYZE=1 ./scripts/compile.sh + - name: Run Tests + run: ./scripts/run_tests.sh + build-windows: strategy: matrix: @@ -47,3 +89,18 @@ jobs: run: ./scripts/compile_cl.bat - name: Run Tests run: ./scripts/run_tests.bat + + build-windows-staticanalyze: + runs-on: windows-latest + if: ${{ false }} + steps: + - uses: actions/checkout@v2 + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64 + - name: Build tests + env: + USE_STATICANALYZE: 1 + run: ./scripts/compile_cl.bat + - name: Run Tests + run: ./scripts/run_tests.bat diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b8816e3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# AGENTS.md + +These instructions apply to the entire repository. + +## Language and Design + +* Prefer C-like C++ ("Orthodox C++"): simple types, explicit ownership, RAII, and clear invariants. +* Target C++98 when possible. +* Do not introduce C++11+ features unless there is a clear need. If newer features are required, guard them and keep a C++98-compatible path when practical. +* Keep dependencies minimal and avoid template-heavy or meta-programming-heavy solutions. +* Do not rely on exceptions (the project builds with `-fno-exceptions`). + +## Style + +* Follow the existing style in nearby files. Do not reformat unrelated code. +* Use 4 spaces for indentation and keep whitespace changes minimal. +* Keep include order consistent with existing files: system headers first, then local headers. +* Prefer project-established forms such as `#if defined(...)` preprocessor guards and uppercase macro names. +* Use simple, readable control flow; avoid clever constructs. + +## Compatibility Guidelines + +* Prefer C/C++98-friendly headers and APIs already used in this repo (``, ``, etc.) unless there is a reason to use something else. +* Avoid unguarded use of features like `nullptr`, `auto`, lambdas, `override`, variadic templates, and `static_assert`. +* Keep code warning-clean under the current compile flags. + +## Validation + +* Build with C++98 first when touching C++ code: + * `STDVERSION=c++98 scripts/compile.sh` +* Run tests after changes: + * `scripts/run_tests.sh` diff --git a/scripts/compile.sh b/scripts/compile.sh index ebea8cb..4fc0705 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -8,14 +8,47 @@ fi #DISASSEMBLY="-S -masm=intel" #PREPROCESS="-E" +if [ "$CXX" == "" ]; then + CXX=clang++ +fi + +if [ "$USE_STATICANALYZE" != "" ]; then + if [ "$CXX" == "clang++" ]; then + STATIC_ANALYZER_CLANG=1 + STATIC_ANALYZER_FLAGS="-Xanalyzer -analyzer-output=text -Xanalyzer -analyzer-werror -Xanalyzer -analyzer-disable-checker -Xanalyzer deadcode.DeadStores" + echo "Using STATIC ANALYZER (clang)" + else + echo "USE_STATICANALYZE requires clang++ in scripts/compile.sh" + exit 1 + fi +fi + if [ "$USE_ASAN" != "" ]; then if [ "$CXX" != "g++" ]; then - ASAN="-fsanitize=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -fsanitize=undefined" - ASAN_LDFLAGS="-fsanitize=address " + SANITIZER_CXXFLAGS="$SANITIZER_CXXFLAGS -fsanitize=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope" + SANITIZER_LDFLAGS="$SANITIZER_LDFLAGS -fsanitize=address" echo "Using ASAN" fi fi +if [ "$USE_UBSAN" != "" ]; then + if [ "$CXX" != "g++" ]; then + SANITIZER_CXXFLAGS="$SANITIZER_CXXFLAGS -fsanitize=undefined -fno-omit-frame-pointer" + SANITIZER_LDFLAGS="$SANITIZER_LDFLAGS -fsanitize=undefined" + echo "Using UBSAN" + fi +fi + +if [ "$USE_MSAN" != "" ]; then + if [ "$CXX" != "clang++" ]; then + echo "MSAN requires clang++" + exit 1 + fi + SANITIZER_CXXFLAGS="$SANITIZER_CXXFLAGS -fsanitize=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer" + SANITIZER_LDFLAGS="$SANITIZER_LDFLAGS -fsanitize=memory" + echo "Using MSAN" +fi + if [ "$OPT" == "" ]; then OPT="-O2" fi @@ -27,9 +60,6 @@ fi echo Using -std=$STDVERSION -if [ "$CXX" == "" ]; then - CXX=clang++ -fi echo Using CXX=$CXX $CXX --version @@ -39,17 +69,21 @@ if [ "$ARCH" == "" ]; then fi echo Using ARCH=$ARCH -CXXFLAGS="$CXXFLAGS -std=$STDVERSION -g -Wall -pedantic -fno-exceptions -Werror=format -Isrc -I. $ASAN $PREPROCESS -Wno-old-style-cast" +CXXFLAGS="$CXXFLAGS -std=$STDVERSION -g -Wall -pedantic -fno-exceptions -Werror=format -Isrc -I. $SANITIZER_CXXFLAGS $PREPROCESS -Wno-old-style-cast" if [ "$CXX" == "clang++" ]; then - CXXFLAGS="$CXXFLAGS -Weverything -Wno-global-constructors" + CXXFLAGS="$CXXFLAGS -Weverything -Wno-global-constructors -Wuninitialized -Wsometimes-uninitialized -Wconditional-uninitialized" +fi + +if [ "$CXX" == "g++" ]; then + CXXFLAGS="$CXXFLAGS -Wuninitialized -Wmaybe-uninitialized" fi if [ "$CXX" != "c++98" ]; then CXXFLAGS="$CXXFLAGS -Wno-zero-as-null-pointer-constant -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-suggest-override" fi -LDFLAGS="$LDFLAGS $ASAN_LDFLAGS" +LDFLAGS="$LDFLAGS $SANITIZER_LDFLAGS" @@ -72,9 +106,23 @@ echo "COMPILING WITH JCTEST" PREFIX=jctest EXAMPLE_SOURCE_DIR=./hugo/static/code +ANALYZE_INDEX=0 +ANALYZED_MAIN=0 + +function analyze_source { + local file=$1 + shift + local outfile=./build/analyze_${ANALYZE_INDEX}.o + ANALYZE_INDEX=$((ANALYZE_INDEX + 1)) + echo "Analyzing ${file}" + $CXX --analyze $OPT $ARCH $SYSROOT $CXXFLAGS $STATIC_ANALYZER_FLAGS $* ${file} -o ${outfile} +} function compile_doc_example { local file=example_${1}.cpp + if [ "$STATIC_ANALYZER_CLANG" != "" ]; then + analyze_source ${EXAMPLE_SOURCE_DIR}/${file} + fi echo "Compiling ${file}" $CXX -o ./build/example_${1} $OPT $ARCH $SYSROOT $CXXFLAGS ${EXAMPLE_SOURCE_DIR}/${file} } @@ -82,6 +130,13 @@ function compile_doc_example { function compile_test { local name=$1 shift + if [ "$STATIC_ANALYZER_CLANG" != "" ]; then + analyze_source test/test_${name}.cpp $* + if [ "$ANALYZED_MAIN" == "0" ]; then + analyze_source test/main.cpp + ANALYZED_MAIN=1 + fi + fi echo "Compiling test $name" $CXX -o ./build/${PREFIX}_test_${name}.o $OPT $DISASSEMBLY $ARCH $SYSROOT $CXXFLAGS $* -c test/test_${name}.cpp $CXX -o ./build/${PREFIX}_main.o $OPT $DISASSEMBLY $ARCH $SYSROOT $CXXFLAGS -c test/main.cpp @@ -91,6 +146,9 @@ function compile_test { function compile_test_with_main { local name=$1 shift + if [ "$STATIC_ANALYZER_CLANG" != "" ]; then + analyze_source test/test_${name}.cpp $* + fi echo "Compiling test $name" $CXX -o ./build/${PREFIX}_test_${name}.o $OPT $DISASSEMBLY $ARCH $SYSROOT $CXXFLAGS $* -c test/test_${name}.cpp $CXX -o ./build/${PREFIX}_${name} $OPT $ARCH ./build/${PREFIX}_test_${name}.o $LDFLAGS diff --git a/scripts/compile_cl.bat b/scripts/compile_cl.bat index d387dcc..ae8d235 100644 --- a/scripts/compile_cl.bat +++ b/scripts/compile_cl.bat @@ -1,29 +1,41 @@ -rem echo off - -call python3 --version 2>NUL -if '%errorlevel%'=='1' goto errorNoPython -set TIMEIT=python %~dp0\timeit.py -goto hasPython -:errorNoPython -echo "No python found!" -goto end - -set TIMEIT -:hasPython - -mkdir build - -set FLAGS=/Od /Zi /D_CRT_SECURE_NO_WARNINGS /nologo /D_HAS_EXCEPTIONS=0 /EHsc /W4 /wd4611 /Isrc - -call %TIMEIT% cl.exe %FLAGS% test\test_params.cpp test\main.cpp /link /out:.\build\test_params.exe -call %TIMEIT% cl.exe %FLAGS% test\test_typed_test.cpp test\main.cpp /link /out:.\build\test_typed_test.exe -call %TIMEIT% cl.exe %FLAGS% test\test_expect.cpp test\main.cpp /link /out:.\build\test_expect.exe -call %TIMEIT% cl.exe %FLAGS% test\test_death.cpp test\main.cpp /link /out:.\build\test_death.exe -call %TIMEIT% cl.exe %FLAGS% test\test_empty.cpp test\main.cpp /link /out:.\build\test_empty.exe -call %TIMEIT% cl.exe %FLAGS% test\test_array.cpp test\main.cpp /link /out:.\build\test_array.exe -call %TIMEIT% cl.exe %FLAGS% test\test_color_off.cpp /link /out:.\build\test_color_off.exe -call %TIMEIT% cl.exe %FLAGS% test\test_color_on.cpp /link /out:.\build\test_color_on.exe - -del *.obj - -:end +@echo off + +call python3 --version 2>NUL +if '%errorlevel%'=='1' goto errorNoPython +set TIMEIT=python3 %~dp0\timeit.py +goto hasPython +:errorNoPython +echo "No python found!" +exit /b 1 + +set TIMEIT +:hasPython + +if not exist build mkdir build + +set FLAGS=/Od /Zi /D_CRT_SECURE_NO_WARNINGS /nologo /D_HAS_EXCEPTIONS=0 /EHsc /W4 /wd4611 /Isrc +if not "%USE_STATICANALYZE%"=="" ( + set FLAGS=%FLAGS% /analyze /WX + echo Using STATIC ANALYZER (msvc) +) + +call %TIMEIT% cl.exe %FLAGS% test\test_params.cpp test\main.cpp /link /out:.\build\test_params.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_typed_test.cpp test\main.cpp /link /out:.\build\test_typed_test.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_expect.cpp test\main.cpp /link /out:.\build\test_expect.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_death.cpp test\main.cpp /link /out:.\build\test_death.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_empty.cpp test\main.cpp /link /out:.\build\test_empty.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_array.cpp test\main.cpp /link /out:.\build\test_array.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_color_off.cpp /link /out:.\build\test_color_off.exe +if errorlevel 1 exit /b %errorlevel% +call %TIMEIT% cl.exe %FLAGS% test\test_color_on.cpp /link /out:.\build\test_color_on.exe +if errorlevel 1 exit /b %errorlevel% + +del *.obj + +exit /b 0 diff --git a/scripts/run_tests.bat b/scripts/run_tests.bat index 2afefdc..e296489 100644 --- a/scripts/run_tests.bat +++ b/scripts/run_tests.bat @@ -1,11 +1,20 @@ -echo off - -.\build\test_params.exe -.\build\test_typed_test.exe -.\build\test_expect.exe -.\build\test_death.exe -.\build\test_empty.exe -.\build\test_array.exe -.\build\test_doctest.exe -.\build\test_color_off.exe -.\build\test_color_on.exe +@echo off + +call :run_test .\build\test_params.exe +call :run_test .\build\test_typed_test.exe +call :run_test .\build\test_expect.exe +call :run_test .\build\test_death.exe +call :run_test .\build\test_empty.exe +call :run_test .\build\test_array.exe +call :run_test .\build\test_color_off.exe +call :run_test .\build\test_color_on.exe +exit /b 0 + +:run_test +if not exist "%~1" ( + echo Missing test executable: %~1 + exit /b 1 +) +call "%~1" +if errorlevel 1 exit /b %errorlevel% +exit /b 0 diff --git a/scripts/timeit.py b/scripts/timeit.py index 1c912df..5b1bc0d 100644 --- a/scripts/timeit.py +++ b/scripts/timeit.py @@ -6,3 +6,4 @@ p.wait() tend = time.time() print("%s took %f ms" % (sys.argv[1], (tend-tstart)*1000.0)) + sys.exit(p.returncode) diff --git a/src/jc_test.h b/src/jc_test.h index a4aaa7a..b10d1d4 100644 --- a/src/jc_test.h +++ b/src/jc_test.h @@ -732,7 +732,7 @@ struct jc_test_register_typed_class_test { template jc_test_fixture* jc_test_alloc_fixture_with_param(const char* name, unsigned int type) { - return jc_test_create_fixture(new jc_test_fixture_with_param, name, type); + return jc_test_create_fixture(new jc_test_fixture_with_param(), name, type); } template @@ -753,10 +753,11 @@ void jc_test_create_from_prototype(jc_test_fixture_with_param* fixtur jc_test_entry* first = 0; jc_test_entry* prev = 0; while (prototype_test) { - jc_test_entry* test = new jc_test_entry; + jc_test_entry* test = new jc_test_entry(); test->next = 0; test->name = prototype_test->name; test->factory = 0; + test->time = 0; test->fail = 0; test->skipped = 0; @@ -786,6 +787,10 @@ int jc_test_register_param_tests(const char* prototype_fixture_name, const char* // Allocate a new fixture, and create the test class jc_test_fixture_with_param* fixture = JC_TEST_CAST(jc_test_fixture_with_param*, jc_test_alloc_fixture_with_param(fixture_name, JC_TEST_FIXTURE_TYPE_CLASS) ); + if (!fixture) { + delete values; + return 1; + } fixture->first = first_fixture == 0 ? 1 : 0; if (!first_fixture) { @@ -934,7 +939,12 @@ struct jc_buffered_string void Grow(size_t _size) { capacity += _size; +#if defined(_MSC_VER) + // C6308: realloc may return null and overwrite the original pointer, causing a leak. + #pragma warning(suppress:6308) +#endif buffer = (char*)realloc(buffer, capacity); + assert(buffer != 0); } void Append(const char* str, size_t len) @@ -1042,7 +1052,7 @@ template <> char* jc_test_print_value(char* buffer, size_t buffer_len, std::null static int jc_get_formatted_test_name(char* buffer, size_t buffer_len, const jc_test_fixture* fixture, const jc_test_entry* test, int usecolor) { if (fixture->index != 0xFFFFFFFF) - return JC_TEST_SNPRINTF(buffer, buffer_len, "%s%s%s.%s%s%s/%d", JC_TEST_COL2(CYAN,usecolor), fixture->name, JC_TEST_COL2(DEFAULT,usecolor), JC_TEST_COL2(YELLOW,usecolor), test->name, JC_TEST_COL2(DEFAULT,usecolor), fixture->index); + return JC_TEST_SNPRINTF(buffer, buffer_len, "%s%s%s.%s%s%s/%u", JC_TEST_COL2(CYAN,usecolor), fixture->name, JC_TEST_COL2(DEFAULT,usecolor), JC_TEST_COL2(YELLOW,usecolor), test->name, JC_TEST_COL2(DEFAULT,usecolor), fixture->index); else return JC_TEST_SNPRINTF(buffer, buffer_len, "%s%s%s.%s%s%s", JC_TEST_COL2(CYAN,usecolor), fixture->name, JC_TEST_COL2(DEFAULT,usecolor), JC_TEST_COL2(YELLOW,usecolor), test->name, JC_TEST_COL2(DEFAULT,usecolor)); } @@ -1206,7 +1216,7 @@ void jc_test_print_logger::OnTestSetup(const jc_test_fixture* fixture, const jc_ str->Appendf("%s%s%s", JC_TEST_COL(YELLOW), test->name, JC_TEST_COL(DEFAULT)); if (fixture->index != 0xFFFFFFFF) { - str->Appendf("/%d ", fixture->index); + str->Appendf("/%u ", fixture->index); } str->Append("\n"); @@ -1219,7 +1229,7 @@ void jc_test_print_logger::OnTestTeardown(const jc_test_fixture* fixture, const str->Appendf("%s%s%s", JC_TEST_COL(YELLOW), test->name, JC_TEST_COL(DEFAULT)); if (fixture->index != 0xFFFFFFFF) { - str->Appendf("/%d ", fixture->index); + str->Appendf("/%u ", fixture->index); } if (test->fail) str->Appendf(" %s%s%s (", JC_TEST_COL(FAIL), "FAIL", JC_TEST_COL(DEFAULT)); @@ -1436,6 +1446,8 @@ jc_test_fixture* jc_test_create_fixture(jc_test_fixture* fixture, const char* na fixture->next = 0; fixture->tests = 0; fixture->name = name; + fixture->filename = 0; + fixture->prototype = 0; fixture->type = fixture_type; fixture->parent = 0; fixture->fail = 0; @@ -1445,6 +1457,8 @@ jc_test_fixture* jc_test_create_fixture(jc_test_fixture* fixture, const char* na fixture->num_tests = 0; fixture->first = fixture->last = 1; fixture->signum = 0; + fixture->line = 0; + fixture->_pad = 0; fixture->fixture_setup = 0; fixture->fixture_teardown = 0; jc_test_memset(&fixture->stats, 0, sizeof(fixture->stats)); @@ -1459,11 +1473,12 @@ jc_test_fixture* jc_test_create_fixture(jc_test_fixture* fixture, const char* na } jc_test_entry* jc_test_add_test_to_fixture(jc_test_fixture* fixture, const char* test_name, jc_test_base_class* instance, jc_test_factory_base_interface* factory) { - jc_test_entry* test = new jc_test_entry; + jc_test_entry* test = new jc_test_entry(); test->next = 0; test->name = test_name; test->instance = instance; test->factory = factory; + test->time = 0; test->fail = 0; test->skipped = 0; jc_test_entry* prev = fixture->tests; @@ -1487,7 +1502,7 @@ jc_test_fixture* jc_test_find_fixture(const char* name, unsigned int fixture_typ } jc_test_fixture* jc_test_alloc_fixture(const char* name, unsigned int fixture_type) { - return jc_test_create_fixture(new jc_test_fixture, name, fixture_type); + return jc_test_create_fixture(new jc_test_fixture(), name, fixture_type); } int jc_test_register_class_test(const char* fixture_name, const char* test_name, @@ -1848,7 +1863,8 @@ int jc_test_keep_test(jc_test_state* state, const char* name) { if (state->num_filter_patterns == 0) return 1; for (uint32_t i = 0; i < state->num_filter_patterns; ++i) { - if (jc_test_strstr(name, state->filter_patterns[i]) != 0) + const char* pattern = state->filter_patterns[i]; + if (pattern != 0 && jc_test_strstr(name, pattern) != 0) return 1; // it matched the pattern, so let's keep it } return 0; @@ -1869,8 +1885,10 @@ static char* jc_test_strdup(const char* s) { } static void jc_test_add_test_filter(jc_test_state* state, const char* pattern) { - if (state->filter_patterns == 0) + if (state->filter_patterns == 0) { state->filter_patterns = new char*[255]; + jc_test_memset(state->filter_patterns, 0, sizeof(char*) * 255); + } if (state->num_filter_patterns == 255) return; state->filter_patterns[state->num_filter_patterns++] = jc_test_strdup(pattern);