From 00d0df15d7a1a59ffdbb279782f927dcf6e64168 Mon Sep 17 00:00:00 2001 From: Ciao Date: Sun, 28 Jul 2024 20:59:43 +0800 Subject: [PATCH] feat(add): initial feature (#1) * use clang-tidy and clang-format; organize cmake scripts * support unit tests in src * add a dev container; support address sanitizers --- .clang-tidy | 42 ++ .devcontainer/Dockerfile | 15 + .devcontainer/devcontainer.json | 23 + .devcontainer/package.sh | 22 + .github/dependabot.yml | 12 + .gitignore | 6 +- CMakeLists.txt | 31 +- README.md | 19 +- benches/CMakeLists.txt | 15 +- build_support/clang_format_exclusions.txt | 0 build_support/run_clang_format.py | 131 ++++++ build_support/run_clang_tidy.py | 498 ++++++++++++++++++++++ build_support/run_clang_tidy_extra.py | 46 ++ cmake/3rdParty.cmake | 22 + cmake/3rd_party.cmake | 23 - cmake/ClangTools.cmake | 75 ++++ cmake/Config.cmake | 56 +++ cmake/Sanitizers.cmake | 12 + examples/CMakeLists.txt | 30 +- src/CMakeLists.txt | 5 +- src/bin/CMakeLists.txt | 18 + src/bin/demo.cc | 8 + src/include/lib/lib.h | 2 +- src/lib/CMakeLists.txt | 26 +- src/lib/lib.cpp | 5 +- src/lib/lib.test.cpp | 13 + tests/CMakeLists.txt | 15 +- 27 files changed, 1090 insertions(+), 80 deletions(-) create mode 100644 .clang-tidy create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/package.sh create mode 100644 .github/dependabot.yml create mode 100644 build_support/clang_format_exclusions.txt create mode 100755 build_support/run_clang_format.py create mode 100755 build_support/run_clang_tidy.py create mode 100644 build_support/run_clang_tidy_extra.py create mode 100644 cmake/3rdParty.cmake delete mode 100644 cmake/3rd_party.cmake create mode 100644 cmake/ClangTools.cmake create mode 100644 cmake/Config.cmake create mode 100644 cmake/Sanitizers.cmake create mode 100644 src/bin/CMakeLists.txt create mode 100644 src/bin/demo.cc create mode 100644 src/lib/lib.test.cpp diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..06b956b --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,42 @@ +Checks: ' + bugprone-*, + clang-analyzer-*, + performance-*, + portability-*, + readability-*, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-narrowing-conversions, + -bugprone-reserved-identifier, + -bugprone-signed-char-misuse, + -bugprone-suspicious-include, + -bugprone-unhandled-self-assignment, + -clang-analyzer-cplusplus.NewDelete, + -clang-analyzer-cplusplus.NewDeleteLeaks, + -clang-analyzer-security.insecureAPI.rand, + -clang-diagnostic-implicit-int-float-conversion, + -google-readability-avoid-underscore-in-googletest-name, + -modernize-use-nodiscard, + -readability-convert-member-functions-to-static, + -readability-identifier-length, + -readability-function-cognitive-complexity, + -readability-magic-numbers, + -readability-make-member-function-const, + -readability-qualified-auto, + -readability-redundant-access-specifiers, + -bugprone-exception-escape, + ' +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.EnumCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: CamelCase } + - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.MemberCase, value: lower_case } + - { key: readability-identifier-naming.MemberSuffix, value: _ } + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.UnionCase, value: CamelCase } + - { key: readability-identifier-naming.VariableCase, value: lower_case } +WarningsAsErrors: '*' +HeaderFilterRegex: '/(src|tests)/include' +AnalyzeTemporaryDtors: true \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..9e45f5b --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-22.04 + +# Optionally install the cmake for vcpkg +COPY ./package.sh /tmp/ + +RUN chmod +x /tmp/package.sh \ + && /tmp/package.sh \ + && rm -f /tmp/package.sh + +# [Optional] Uncomment this section to install additional vcpkg ports. +# RUN su vscode -c "${VCPKG_ROOT}/vcpkg install " + +# [Optional] Uncomment this section to install additional packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b519e25 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/cpp +{ + "name": "C++", + "build": { + "dockerfile": "Dockerfile" + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "gcc -v", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/package.sh b/.devcontainer/package.sh new file mode 100644 index 0000000..68fb17c --- /dev/null +++ b/.devcontainer/package.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +set -e + +apt-get -y update + +apt-get -y install \ + build-essential \ + clang-12 \ + clang-format-12 \ + clang-tidy-12 \ + cmake \ + doxygen \ + git \ + pkg-config \ + zlib1g-dev \ + libelf-dev \ + libdwarf-dev \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index a5309e6..2a67b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -build*/ +build/ +.idea/ +.vscode/ + +__pycache__/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 53ac665..b92b27c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,8 @@ cmake_minimum_required(VERSION 3.10) -# Descriptions for the project -set(PROJ_NAME cmaker) -set(VER 0.0.1) -set(CPP_STD 17) +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) + +include(Config) project(${PROJ_NAME} VERSION ${VER} LANGUAGES C CXX) @@ -19,6 +18,8 @@ endif(PROJECT_IS_IN_ROOT) option(HAS_TESTS "Build and perform tests" ${PROJECT_IS_IN_ROOT}) option(HAS_BENCHES "Build and perform benches" ${PROJECT_IS_IN_ROOT}) option(HAS_EXAMPLES "Build and perform examples" ${PROJECT_IS_IN_ROOT}) +option(HAS_CLANG_TOOLS "Build and use Clang tools" ${PROJECT_IS_IN_ROOT}) +option(HAS_MSAN "Build and use Memory Sanitizer" ${PROJECT_IS_IN_ROOT}) # Includes set(SRC_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/src/include) @@ -26,11 +27,8 @@ include_directories(${SRC_INCLUDE_DIR}) add_subdirectory(${PROJECT_SOURCE_DIR}/src) -if(PROJECT_IS_IN_ROOT) - include(${CMAKE_SOURCE_DIR}/cmake/3rd_party.cmake) -endif(PROJECT_IS_IN_ROOT) - if(HAS_BENCHES) + include(3rdParty) include_googletest() set(CMAKE_BUILD_TYPE Release) include_benchmark() @@ -40,11 +38,16 @@ endif(HAS_BENCHES) # The below needs to be built in Debug mode set(CMAKE_BUILD_TYPE Debug) -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g") -set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g") + +if(HAS_MSAN) + include(Sanitizers) + message(STATUS "Building Sanitizers") + define_sanitizer() +endif(HAS_MSAN) if(HAS_TESTS) - include_benchmark() + include(3rdParty) + include_googletest() message(STATUS "Building tests") add_subdirectory(tests) endif(HAS_TESTS) @@ -53,3 +56,9 @@ if(HAS_EXAMPLES) message(STATUS "Building examples") add_subdirectory(examples) endif(HAS_EXAMPLES) + +if(HAS_CLANG_TOOLS) + include(ClangTools) + define_clang_tidy_targets() + define_clang_format_targets() +endif(HAS_CLANG_TOOLS) diff --git a/README.md b/README.md index 10249cb..87ff705 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ -# CMaker +

CMaker

+ +CMaker is a CMake template for C/C++ that adopts a Cargo-like package layout. ## What it does -- Make a Cargo for C/C++ from CMake. -- Support C and C++ -- Use [`GoogleTest`](https://github.com/google/googletest) for test -- Use [`Google Benchmark`](https://github.com/google/benchmark) for benchmark +- Designed for C and C++ +- Opt for Cargo-like [`package layout`](https://doc.rust-lang.org/cargo/guide/project-layout.html) in CMake. +- Apply [`GoogleTest`](https://github.com/google/googletest) for test and [`Google Benchmark`](https://github.com/google/benchmark) for benchmark - Use [`Clang-format` and `Clang-tidy`](https://github.com/llvm/llvm-project) for static program analysis (not ready yet) -- Use `Memory Sanitizer` for memory safety check (not ready yet) +- Use `Memory Sanitizer` for memory safety check -## Why do that -As we all know, Cargo might be the best package manager in the computer world. It helps us to create a library or an execuable as ease. In addition, it allows us to run some examples or bins of your choice by the cargo run command. However, Cargo is designed only for Rust progarmming language. If we use C++ more often, how can we configure CMake to function similarly to Cargo? `CMaker` it is. +## Why do this +In my opinion, Cargo might be the best build system in the computer world. It helps us to create a library or an execuable as ease. In addition, it allows us to run some examples or tests of your choice by the cargo run command. However, Cargo is designed only for Rust programming language. If we use C/C++ more often, how can we configure CMake to function similarly to Cargo? `CMaker` is the answer. ## License -CMaker is under [CC0 1.0 Universial](./LICENSE) license, which means it's in the world-wide public domain. \ No newline at end of file +CMaker is under the [CC0 1.0 Universial](./LICENSE) license, which means it's in the world-wide public domain. \ No newline at end of file diff --git a/benches/CMakeLists.txt b/benches/CMakeLists.txt index 1973e37..cff2652 100644 --- a/benches/CMakeLists.txt +++ b/benches/CMakeLists.txt @@ -1,8 +1,10 @@ set(bench_dir_list "${CMAKE_CURRENT_SOURCE_DIR}/lib") +enable_testing() + foreach(bench_dir ${bench_dir_list}) - file(GLOB_RECURSE bench_files "${bench_dir}/*-bench.cpp" "${bench_dir}/*-bench.cc") - get_filename_component(dir_name ${bench_dir} NAME) + file(GLOB_RECURSE bench_files "${bench_dir}/*.cpp" "${bench_dir}/*.cc") + get_filename_component(dir_name ${bench_dir} NAME_WE) set(bench_name ${dir_name}_bench) add_executable(${bench_name} EXCLUDE_FROM_ALL ${bench_files}) @@ -13,7 +15,14 @@ foreach(bench_dir ${bench_dir_list}) COMMENT "Running bench ${bench_name}..." COMMAND ${bench_name} ) - set_target_properties(${bench_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/benches") + set_target_properties(${bench_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${BenchesBinaryPath}") list(APPEND BENCH_TARGETS ${bench_name}) + add_test(NAME ${bench_name} COMMAND ${bench_name}) endforeach(bench_dir ${bench_dir_list}) + +add_custom_target(run-benches + COMMAND ${CMAKE_CTEST_COMMAND} -C Debug --verbose + DEPENDS ${BENCH_TARGETS} + COMMENT "Building and runnig all benches..." +) diff --git a/build_support/clang_format_exclusions.txt b/build_support/clang_format_exclusions.txt new file mode 100644 index 0000000..e69de29 diff --git a/build_support/run_clang_format.py b/build_support/run_clang_format.py new file mode 100755 index 0000000..3af555c --- /dev/null +++ b/build_support/run_clang_format.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# Modified from the Apache Arrow project for the Terrier project. + +import argparse +import codecs +import difflib +import fnmatch +import os +import subprocess +import sys + + +def has_correct_extensions(filename, extensions=(".h", ".hh", ".cc", ".cpp", ".inc")): + return filename.endswith(extensions) + + +def check(arguments, source_dir): + formatted_filenames = [] + error = False + for directory, subdirs, filenames in os.walk(source_dir): + fullpaths = (os.path.join(directory, filename) + for filename in filenames) + source_files = [x for x in fullpaths + if has_correct_extensions(x)] + formatted_filenames.extend( + # Filter out files that match the globs in the globs file + [filename for filename in source_files + if not any((fnmatch.fnmatch(filename, exclude_glob) + for exclude_glob in exclude_globs))]) + + if arguments.fix: + if not arguments.quiet: + # Print out each file on its own line, but run + # clang format once for all of the files + print("\n".join(map(lambda x: "Formatting {}".format(x), + formatted_filenames))) + subprocess.check_call([arguments.clang_format_binary, + "-i"] + formatted_filenames) + else: + for filename in formatted_filenames: + if not arguments.quiet: + print("Checking {}".format(filename)) + # + # Due to some incompatibilities between Python 2 and + # Python 3, there are some specific actions we take here + # to make sure the difflib.unified_diff call works. + # + # In Python 2, the call to subprocess.check_output return + # a 'str' type. In Python 3, however, the call returns a + # 'bytes' type unless the 'encoding' argument is + # specified. Unfortunately, the 'encoding' argument is not + # in the Python 2 API. We could do an if/else here based + # on the version of Python we are running, but it's more + # straightforward to read the file in binary and do utf-8 + # conversion. In Python 2, it's just converting string + # types to unicode types, whereas in Python 3 it's + # converting bytes types to utf-8 encoded str types. This + # approach ensures that the arguments to + # difflib.unified_diff are acceptable string types in both + # Python 2 and Python 3. + with open(filename, "rb") as reader: + # Run clang-format and capture its output + formatted = subprocess.check_output( + [arguments.clang_format_binary, + filename]) + formatted = codecs.decode(formatted, "utf-8") + # Read the original file + original = codecs.decode(reader.read(), "utf-8") + # Run the equivalent of diff -u + diff = list(difflib.unified_diff( + original.splitlines(True), + formatted.splitlines(True), + fromfile=filename, + tofile="{} (after clang format)".format( + filename))) + if diff: + print("{} had clang-format style issues".format(filename)) + # Print out the diff to stderr + error = True + sys.stderr.writelines(diff) + return error + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Runs clang format on all of the source " + "files. If --fix is specified, and compares the output " + "with the existing file, outputting a unifiied diff if " + "there are any necessary changes") + parser.add_argument("clang_format_binary", + help="Path to the clang-format binary") + parser.add_argument("exclude_globs", + help="Filename containing globs for files " + "that should be excluded from the checks") + parser.add_argument("--source_dirs", + help="Comma-separated root directories of the code") + parser.add_argument("--fix", default=False, + action="store_true", + help="If specified, will re-format the source " + "code instead of comparing the re-formatted " + "output, defaults to %(default)s") + parser.add_argument("--quiet", default=False, + action="store_true", + help="If specified, only print errors") + + args = parser.parse_args() + + had_err = False + exclude_globs = [line.strip() for line in open(args.exclude_globs)] + for source_dir in args.source_dirs.split(','): + if len(source_dir) > 0: + had_err = had_err or check(args, source_dir) + + sys.exit(1 if had_err else 0) \ No newline at end of file diff --git a/build_support/run_clang_tidy.py b/build_support/run_clang_tidy.py new file mode 100755 index 0000000..2172586 --- /dev/null +++ b/build_support/run_clang_tidy.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +# +# ===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===# +# +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. +# +# Modified from the LLVM project for the Terrier project. +# +# Added `only_diff` cli arg to run clang-tidy on git diff. +# ===------------------------------------------------------------------------===# +# FIXME: Integrate with clang-tidy-diff.py + +""" +Parallel clang-tidy runner +========================== +Runs clang-tidy over all files in a compilation database. Requires clang-tidy +and clang-apply-replacements in $PATH. +Example invocations. +- Run clang-tidy on all files in the current working directory with a default + set of checks and show warnings in the cpp files and all project headers. + run-clang-tidy.py $PWD +- Fix all header guards. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard +- Fix all header guards included from clang-tidy and header guards + for clang-tidy headers. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ + -header-filter=extra/clang-tidy +Compilation database setup: +http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html +""" + +from __future__ import division +from __future__ import print_function + +import argparse +import glob +import json +import multiprocessing +import os +import pprint # TERRIER: we want to print out formatted lists of files +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback + +# import yaml # TERRIER: not necessary if we don't want automatic fixes + +from run_clang_tidy_extra import CheckConfig + +is_py2 = sys.version[0] == "2" + +if is_py2: + import Queue as queue +else: + import queue as queue + + +def find_compilation_database(path): + """Adjusts the directory until a compilation database is found.""" + result = "./" + while not os.path.isfile(os.path.join(result, path)): + if os.path.realpath(result) == "/": + print("Error: could not find compilation database.") + sys.exit(1) + result += "../" + return os.path.realpath(result) + + +def make_absolute(f, directory): + if os.path.isabs(f): + return f + return os.path.normpath(os.path.join(directory, f)) + + +def supports_color(): + """ + Modified from https://github.com/django/django/blob/main/django/core/management/color.py + Return True if the running system's terminal supports color, + and False otherwise. + """ + + # isatty is not always implemented, #6223. + is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + + return is_a_tty and ( + sys.platform != "win32" + or "ANSICON" in os.environ + or + # Windows Terminal supports VT codes. + "WT_SESSION" in os.environ + or + # Microsoft Visual Studio Code's built-in terminal supports colors. + os.environ.get("TERM_PROGRAM") == "vscode" + ) + + +def get_tidy_invocation( + f, + clang_tidy_binary, + checks, + tmpdir, + build_path, + header_filter, + extra_arg, + extra_arg_before, + quiet, + config, +): + """Gets a command line for clang-tidy.""" + start = [clang_tidy_binary] + + if supports_color(): + start.append("--use-color") + + if header_filter is not None: + start.append("-header-filter=" + header_filter) + else: + # Show warnings in all in-project headers by default. + # start.append('-header-filter=^' + build_path + '/.*') + # TERRIER: we have our .clang-tidy file + pass + if checks: + start.append("-checks=" + checks) + if tmpdir is not None: + start.append("-export-fixes") + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir) + os.close(handle) + start.append(name) + for arg in extra_arg: + start.append("-extra-arg=%s" % arg) + for arg in extra_arg_before: + start.append("-extra-arg-before=%s" % arg) + start.append("-p=" + build_path) + if quiet: + start.append("-quiet") + if config: + start.append("-config=" + config) + start.append(f) + return start + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = "Diagnostics" + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")): + content = yaml.safe_load(open(replacefile, "r")) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {"MainSourceFile": "", mergekey: merged} + with open(mergefile, "w") as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, "w").close() + + +def check_clang_apply_replacements_binary(args): + """Checks if invoking supplied clang-apply-replacements binary works.""" + try: + subprocess.check_call([args.clang_apply_replacements_binary, "--version"]) + except: + print( + "Unable to run clang-apply-replacements. Is clang-apply-replacements " + "binary correctly specified?", + file=sys.stderr, + ) + traceback.print_exc() + sys.exit(1) + + +def apply_fixes(args, tmpdir): + """Calls clang-apply-fixes on a given directory.""" + invocation = [args.clang_apply_replacements_binary] + if args.format: + invocation.append("-format") + if args.style: + invocation.append("-style=" + args.style) + invocation.append(tmpdir) + subprocess.call(invocation) + + +def run_tidy(args, tmpdir, build_path, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-tidy on them.""" + while True: + name = queue.get() + print("Checking: {}".format(name)) + sys.stdout.flush() + invocation = get_tidy_invocation( + name, + args.clang_tidy_binary, + args.checks, + tmpdir, + build_path, + args.header_filter, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config, + ) + cc = CheckConfig() + # name is the full path of the file for clang-tidy to check + if cc.should_skip(name): + queue.task_done() + continue + + proc = subprocess.Popen( + invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + output, err = proc.communicate() + if proc.returncode != 0: + failed_files.append(name) + # TERRIER: we write our own printing logic + # with lock: + # sys.stdout.write(' '.join(invocation) + '\n' + output + '\n') + # if err > 0: + # sys.stderr.write(err + '\n') + # In particular, we only want important lines: + with lock: + output = output.decode("utf-8") if output is not None else None + err = err.decode("utf-8") if output is not None else None + # unfortunately, our error messages are actually on STDOUT + # STDERR tells how many warnings are generated, + # but this includes non-user-code warnings, so it is useless... + if output: + sys.stdout.write("\n") + sys.stdout.write(output) + queue.task_done() + + +def main(): + parser = argparse.ArgumentParser( + description="Runs clang-tidy over all files " + "in a compilation database. Requires " + "clang-tidy and clang-apply-replacements in " + "$PATH." + ) + parser.add_argument( + "-clang-tidy-binary", + metavar="PATH", + default="clang-tidy", + help="path to clang-tidy binary", + ) + parser.add_argument( + "-clang-apply-replacements-binary", + metavar="PATH", + default="clang-apply-replacements", + help="path to clang-apply-replacements binary", + ) + parser.add_argument( + "-checks", + default=None, + help="checks filter, when not specified, use clang-tidy " "default", + ) + parser.add_argument( + "-config", + default=None, + help="Specifies a configuration in YAML/JSON format: " + " -config=\"{Checks: '*', " + " CheckOptions: [{key: x, " + ' value: y}]}" ' + "When the value is empty, clang-tidy will " + "attempt to find a file named .clang-tidy for " + "each source file in its parent directories.", + ) + parser.add_argument( + "-header-filter", + default=None, + help="regular expression matching the names of the " + "headers to output diagnostics from. Diagnostics from " + "the main file of each translation unit are always " + "displayed.", + ) + parser.add_argument( + "-export-fixes", + metavar="filename", + dest="export_fixes", + help="Create a yaml file to store suggested fixes in, " + "which can be applied with clang-apply-replacements.", + ) + parser.add_argument( + "-j", + type=int, + default=0, + help="number of tidy instances to be run in parallel.", + ) + parser.add_argument( + "files", nargs="*", default=[".*"], help="files to be processed (regex on path)" + ) + parser.add_argument("-fix", action="store_true", help="apply fix-its") + parser.add_argument( + "-format", action="store_true", help="Reformat code " "after applying fixes" + ) + parser.add_argument( + "-style", + default="file", + help="The style of reformat " "code after applying fixes", + ) + parser.add_argument( + "-p", dest="build_path", help="Path used to read a compile command database." + ) + parser.add_argument( + "-extra-arg", + dest="extra_arg", + action="append", + default=[], + help="Additional argument to append to the compiler " "command line.", + ) + parser.add_argument( + "-extra-arg-before", + dest="extra_arg_before", + action="append", + default=[], + help="Additional argument to prepend to the compiler " "command line.", + ) + parser.add_argument( + "-quiet", action="store_true", help="Run clang-tidy in quiet mode" + ) + parser.add_argument( + "-only-diff", + action="store_true", + help="Only run clang-tidy on diff file to master branch", + ) + args = parser.parse_args() + + db_path = "compile_commands.json" + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + try: + invocation = [args.clang_tidy_binary, "-list-checks"] + invocation.append("-p=" + build_path) + if args.checks: + invocation.append("-checks=" + args.checks) + invocation.append("-") + subprocess.check_call(invocation) + except: + print("Unable to run clang-tidy.", file=sys.stderr) + sys.exit(1) + + # Load the database and extract all files. + database = json.load(open(os.path.join(build_path, db_path))) + files = [make_absolute(entry["file"], entry["directory"]) for entry in database] + + # Running clang-tidy in the whole project is slow. Therefore, we added + # support for running clang-tidy on git diff. When `only_diff` is + # specified in the command line, we only check files modified compared + # with origin/master, so as to speed up clang-tidy check. + # + # This functionality is set as CMake target `check-clang-tidy-diff`. + # You can use `make check-clang-tidy-diff` to do a fast clang-tidy + # check. + if args.only_diff: + # Get the path of the repo, e.g. /Users/terrier/bustub + git_repo = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], capture_output=True + ) + git_repo_path = git_repo.stdout.decode("utf-8").strip() + # Get all files changed compared with origin/master + result = subprocess.run( + ["git", "--no-pager", "diff", "--name-only", "origin/master"], + capture_output=True, + ) + git_changed_file_list = list( + map( + lambda x: make_absolute(x, git_repo_path), + result.stdout.decode("utf-8").strip().split("\n"), + ) + ) + git_changed_file_set = set(git_changed_file_list) + # Only retain files that exists in git diff + files = list(filter(lambda x: x in git_changed_file_set, files)) + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + tmpdir = None + if args.fix or args.export_fixes: + check_clang_apply_replacements_binary(args) + tmpdir = tempfile.mkdtemp() + + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile("|".join(args.files)) + + return_code = 0 + try: + # Spin up a bunch of tidy-launching threads. + task_queue = queue.Queue(max_task) + # List of files with a non-zero return code. + failed_files = [] + lock = threading.Lock() + for _ in range(max_task): + t = threading.Thread( + target=run_tidy, + args=(args, tmpdir, build_path, task_queue, lock, failed_files), + ) + t.daemon = True + t.start() + + def update_progress(current_file, num_files): + pct = int(current_file / num_files * 100) + if current_file == num_files or pct % max(2, num_files // 10) == 0: + stars = pct // 10 + spaces = 10 - pct // 10 + print( + "\rProgress: [{}{}] ({}% / File {} of {})".format( + "x" * stars, " " * spaces, pct, current_file, num_files + ), + end="", + ) + sys.stdout.flush() + if current_file == num_files: + print() + + # Fill the queue with files. + for i, name in enumerate(files): + if file_name_re.search(name): + if name.endswith("tools/backtrace.cpp"): + continue + put_file = False + while not put_file: + try: + task_queue.put(name, block=True, timeout=300) + put_file = True + # update_progress(i, len(files)) + except queue.Full: + print("Still waiting to put files into clang-tidy queue.") + sys.stdout.flush() + + # Wait for all threads to be done. + task_queue.join() + # update_progress(100, 100) + if len(failed_files): + return_code = 1 + # TERRIER: We want to see the failed files + print("The files that failed were:") + print(pprint.pformat(failed_files)) + print( + "Note that a failing .h file will fail all the .cpp files that include it.\n" + ) + + except KeyboardInterrupt: + # This is a sad hack. Unfortunately subprocess goes + # bonkers with ctrl-c and we start forking merrily. + print("\nCtrl-C detected, goodbye.") + if tmpdir: + shutil.rmtree(tmpdir) + os.kill(0, 9) + + if args.export_fixes: + print("Writing fixes to " + args.export_fixes + " ...") + try: + merge_replacement_files(tmpdir, args.export_fixes) + except: + print("Error exporting fixes.\n", file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if args.fix: + print("Applying fixes ...") + try: + apply_fixes(args, tmpdir) + except: + print("Error applying fixes.\n", file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if tmpdir: + shutil.rmtree(tmpdir) + print("") + sys.stdout.flush() + sys.exit(return_code) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/build_support/run_clang_tidy_extra.py b/build_support/run_clang_tidy_extra.py new file mode 100644 index 0000000..3c0de04 --- /dev/null +++ b/build_support/run_clang_tidy_extra.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# Modified from the bustub project. + +""" +A helper class, to suppress execution of clang-tidy. + +In clang-tidy-6.0, if the clang-tidy configuration file suppresses ALL checks, +(e.g. via a .clang-tidy file), clang-tidy will print usage information and +exit with a return code of 0. Harmless but verbose. In later versions of +clang-tidy the return code becomes 1, making this a bigger problem. + +This helper addresses the problem by suppressing execution according to +the configuration in this file. +""" + +import re + +class CheckConfig(object): + """ Check paths against the built-in config """ + + def __init__(self): + self._init_config() + # debug prints + self.debug = False + return + + def _init_config(self): + """ Any path matching one of the ignore_pats regular expressions, + denotes that we do NOT want to run clang-tidy on that item. + """ + self.ignore_pats = [".*/third_party/.*", ] + return + + def should_skip(self, path): + """ Should execution of clang-tidy be skipped? + path - to check, against the configuration. + Typically the full path. + returns - False if we want to run clang-tidy + True of we want to skip execution on this item + """ + for pat in self.ignore_pats: + if re.match(pat, path): + if self.debug: + print("match pat: {}, {} => don't run".format(pat, path)) + return True + return False diff --git a/cmake/3rdParty.cmake b/cmake/3rdParty.cmake new file mode 100644 index 0000000..d6a5375 --- /dev/null +++ b/cmake/3rdParty.cmake @@ -0,0 +1,22 @@ +set(googletest_version 1.15.0) +set(benchmark_version 1.8.5) + +include(FetchContent) + +FetchContent_Declare( + googletest + URL "https://github.com/google/googletest/archive/refs/tags/v${googletest_version}.zip" +) + +FetchContent_Declare( + benchmark + URL "https://github.com/google/benchmark/archive/refs/tags/v${benchmark_version}.zip" +) + +function(include_googletest) + FetchContent_MakeAvailable(googletest) +endfunction(include_googletest) + +function(include_benchmark) + FetchContent_MakeAvailable(benchmark) +endfunction(include_benchmark) diff --git a/cmake/3rd_party.cmake b/cmake/3rd_party.cmake deleted file mode 100644 index f870fd2..0000000 --- a/cmake/3rd_party.cmake +++ /dev/null @@ -1,23 +0,0 @@ -set(googletest_version 1.15.0) -set(benchmark_version 1.8.5) - -function(include_googletest) - include(FetchContent) - FetchContent_Declare( - googletest - URL "https://github.com/google/googletest/archive/refs/tags/v${googletest_version}.zip" - ) - - FetchContent_MakeAvailable(googletest) -endfunction(include_googletest) - -function(include_benchmark) - include(FetchContent) - - FetchContent_Declare( - benchmark - URL "https://github.com/google/benchmark/archive/refs/tags/v${benchmark_version}.zip" - ) - - FetchContent_MakeAvailable(benchmark) -endfunction(include_benchmark) diff --git a/cmake/ClangTools.cmake b/cmake/ClangTools.cmake new file mode 100644 index 0000000..90816ed --- /dev/null +++ b/cmake/ClangTools.cmake @@ -0,0 +1,75 @@ +set(CLANG_TOOLS_VERSION 12) +set(BUILD_SUPPORT_DIR "${CMAKE_SOURCE_DIR}/build_support") +set(CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/usr/local/opt/llvm/bin" "/usr/local/opt/llvm@${CLANG_TOOLS_VERSION}/bin" + "/opt/homebrew/opt/llvm@${CLANG_TOOLS_VERSION}/bin/") + +# target defined: check-clang-tidy, check-clang-diff-diff +function(define_clang_tidy_targets) + find_program(CLANG_TIDY_BIN + NAMES clang-tidy "clang-tidy-${CLANG_TOOLS_VERSION}" + HINTS ${CLANG_SEARCH_PATH}) + + if("${CLANG_TIDY_BIN}" STREQUAL "CLANG_TIDY_BIN-NOTFOUND") + message(WARNING "${PROJECT_NAME} couldn't find clang-tidy.") + else() + set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + message(STATUS "${PROJECT_NAME} found clang-tidy at ${CLANG_TIDY_BIN}") + endif("${CLANG_TIDY_BIN}" STREQUAL "CLANG_TIDY_BIN-NOTFOUND") + + add_custom_target(check-clang-tidy + ${BUILD_SUPPORT_DIR}/run_clang_tidy.py # run LLVM's clang-tidy script + -clang-tidy-binary ${CLANG_TIDY_BIN} # using our clang-tidy binary + -p ${CMAKE_BINARY_DIR} # using cmake's generated compile commands + ) + add_custom_target(fix-clang-tidy + ${BUILD_SUPPORT_DIR}/run_clang_tidy.py # run LLVM's clang-tidy script + -clang-tidy-binary ${CLANG_TIDY_BIN} # using our clang-tidy binary + -p ${CMAKE_BINARY_DIR} # using cmake's generated compile commands + -clang-apply-replacements-binary ${CLANG_APPLY_REPLACEMENTS_BIN} # using our clang-apply-replacements binary + -fix # apply suggested changes generated by clang-tidy + ) + add_custom_target(check-clang-tidy-diff + ${BUILD_SUPPORT_DIR}/run_clang_tidy.py # run LLVM's clang-tidy script + -clang-tidy-binary ${CLANG_TIDY_BIN} # using our clang-tidy binary + -p ${CMAKE_BINARY_DIR} # using cmake's generated compile commands + -only-diff # only check diff files to master + ) +endfunction(define_clang_tidy_targets) + +function(define_clang_format_targets) + find_program(CLANG_FORMAT_BIN + NAMES clang-format "clang-format-${CLANG_TOOLS_VERSION}" + HINTS ${CLANG_SEARCH_PATH}) + + if("${CLANG_FORMAT_BIN}" STREQUAL "CLANG_FORMAT_BIN-NOTFOUND") + message(WARNING "${PROJECT_NAME} couldn't find clang-format.") + else() + message(STATUS "${PROJECT_NAME} found clang-format at ${CLANG_FORMAT_BIN}") + endif() + + string(CONCAT FORMAT_DIRS + "${CMAKE_CURRENT_SOURCE_DIR}/src," + "${CMAKE_CURRENT_SOURCE_DIR}/tests," + "${CMAKE_CURRENT_SOURCE_DIR}/benches," + "${CMAKE_CURRENT_SOURCE_DIR}/examples," + ) + # Runs clang format and updates files in place. + add_custom_target(format ${BUILD_SUPPORT_DIR}/run_clang_format.py + ${CLANG_FORMAT_BIN} + ${BUILD_SUPPORT_DIR}/clang_format_exclusions.txt + --source_dirs + ${FORMAT_DIRS} + --fix + --quiet + ) + + # Runs clang format and exits with a non-zero exit code if any files need to be reformatted + add_custom_target(check-format ${BUILD_SUPPORT_DIR}/run_clang_format.py + ${CLANG_FORMAT_BIN} + ${BUILD_SUPPORT_DIR}/clang_format_exclusions.txt + --source_dirs + ${FORMAT_DIRS} + --quiet + ) +endfunction(define_clang_format_targets) + diff --git a/cmake/Config.cmake b/cmake/Config.cmake new file mode 100644 index 0000000..862ba87 --- /dev/null +++ b/cmake/Config.cmake @@ -0,0 +1,56 @@ +# Descriptions for the project +set(PROJ_NAME cmaker) +set(VER 0.0.1) +set(CPP_STD 17) + +set(unit_test_pattern ".test.") +set(supported_suffixes "cc" "cpp" "cxx" "c") + +# Objects definition +set(ALL_OBJECTS "") +set(CMAKE_BUILD_TYPE Release) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-parameter -Wno-attributes") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-attributes") + +# Paths +set(UnitTestsBinaryPath "${CMAKE_BINARY_DIR}/unit-tests") +set(TestsBinaryPath "${CMAKE_BINARY_DIR}/tests") +set(BenchesBinaryPath "${CMAKE_BINARY_DIR}/benches") +set(ExamplesBinaryPath "${CMAKE_BINARY_DIR}/examples") +set(BinBinaryPath "${CMAKE_BINARY_DIR}/bin") + +# Functions definition +function(append_to_list_elements list_out item pattern) + foreach(suffix ${supported_suffixes}) + list(APPEND ret_list "${item}/*${pattern}${suffix}") + endforeach(suffix ${supported_suffixes}) + set(${list_out} ${ret_list} PARENT_SCOPE) +endfunction(append_to_list_elements list_out item pattern) + +function(get_current_directory_name dir_name cur_dir) + get_filename_component(name ${cur_dir} NAME) + set(${dir_name} ${name} PARENT_SCOPE) +endfunction(get_current_directory_name dir_name cur_dir) + +function(get_source_files output_var curr_dir) + append_to_list_elements(glob_patterns ${curr_dir} ".") + file(GLOB all_files ${glob_patterns}) + + set(filtered_files) + + foreach(file ${all_files}) + if(NOT file MATCHES ${unit_test_pattern}) + list(APPEND filtered_files ${file}) + endif(NOT file MATCHES ${unit_test_pattern}) + endforeach(file ${all_files}) + + set(${output_var} ${filtered_files} PARENT_SCOPE) +endfunction(get_source_files output_var curr_dir) + +function(get_test_files output_var curr_dir) + append_to_list_elements(glob_patterns ${curr_dir} "${unit_test_pattern}") + file(GLOB all_files ${glob_patterns}) + set(${output_var} ${all_files} PARENT_SCOPE) +endfunction(get_test_files output_var curr_dir) diff --git a/cmake/Sanitizers.cmake b/cmake/Sanitizers.cmake new file mode 100644 index 0000000..8e3b172 --- /dev/null +++ b/cmake/Sanitizers.cmake @@ -0,0 +1,12 @@ +function(define_sanitizer) + if(NOT DEFINED SANITIZER) + set(SANITIZER address) + endif() + + message(STATUS "Build mode: ${CMAKE_BUILD_TYPE}") + message(STATUS "${SANITIZER} sanitizer will be enabled in debug mode.") + + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -ggdb -fsanitize=${SANITIZER} -fno-omit-frame-pointer -fno-optimize-sibling-calls" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -ggdb -fsanitize=${SANITIZER} -fno-omit-frame-pointer -fno-optimize-sibling-calls" PARENT_SCOPE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endfunction(define_sanitizer) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4b0057e..cc977b7 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,28 +1,28 @@ -file(GLOB_RECURSE EXAMPLE_SOURCES "${PROJECT_SOURCE_DIR}/examples/*.cc" "${PROJECT_SOURCE_DIR}/examples/*.cpp") - -add_custom_target(example) +get_source_files(EXAMPLE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}") +add_custom_target(run-examples) foreach (example_source ${EXAMPLE_SOURCES}) - get_filename_component(example_filename ${example_source} NAME) - add_executable(${example_filename} EXCLUDE_FROM_ALL ${example_source}) + get_filename_component(example_filename ${example_source} NAME_WE) + set(example_name "${example_filename}_example") + add_executable(${example_name} EXCLUDE_FROM_ALL ${example_source}) add_custom_command( - TARGET example - COMMENT "Running example ${example_filename}..." - COMMAND $ + TARGET run-examples + COMMENT "Running all examples..." + COMMAND $ USES_TERMINAL ) - target_link_libraries(${example_filename} PRIVATE lib_obj) + target_link_libraries(${example_name} PRIVATE ${ALL_OBJECTS}) - set_target_properties(${example_filename} + set_target_properties(${example_name} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples" - COMMAND ${example_filename} + RUNTIME_OUTPUT_DIRECTORY "${ExamplestsBinaryPath}" + COMMAND ${example_name} ) - add_custom_target("example-${example_filename}" - COMMENT "Running example ${example_filename}..." - COMMAND ${example_filename} + add_custom_target(example-${example_filename} + COMMENT "Running example ${example_name}..." + COMMAND ${example_name} ) endforeach () diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 99817c3..53e184a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,8 @@ add_subdirectory(lib) +# TODO support dynamic library add_library(${PROJECT_NAME} STATIC ${ALL_OBJECT_FILES}) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +set(ALL_OBJECTS ${ALL_OBJECTS} PARENT_SCOPE) -# TODO -# add_subdirectory(bin) +add_subdirectory(bin) diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt new file mode 100644 index 0000000..001691d --- /dev/null +++ b/src/bin/CMakeLists.txt @@ -0,0 +1,18 @@ +get_source_files(bin_files "${CMAKE_CURRENT_SOURCE_DIR}") + +foreach(bin_file ${bin_files}) + get_filename_component(bin_name ${bin_file} NAME_WE) + add_executable(${bin_name} ${bin_file}) + target_link_libraries(${bin_name} PRIVATE ${ALL_OBJECTS}) + + set_target_properties(${bin_name} + PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${BinBinaryPath}" + COMMAND ${bin_name} + ) + + add_custom_target( + "bin-${bin_name}" + COMMENT "Running bin ${bin_file}..." + COMMAND ${bin_name} + ) +endforeach(bin_file ${bin_files}) diff --git a/src/bin/demo.cc b/src/bin/demo.cc new file mode 100644 index 0000000..31cb315 --- /dev/null +++ b/src/bin/demo.cc @@ -0,0 +1,8 @@ +#include "lib/lib.h" + +#include + +int main(int argc, char const *argv[]) { + std::cout << add(1, 2) << '\n'; + return 0; +} diff --git a/src/include/lib/lib.h b/src/include/lib/lib.h index d866144..ac7e3e2 100644 --- a/src/include/lib/lib.h +++ b/src/include/lib/lib.h @@ -4,4 +4,4 @@ template T add(T a, T b); -#endif //__LIB_H__ \ No newline at end of file +#endif //__LIB_H__ \ No newline at end of file diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 49b4626..ac8739b 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -1,9 +1,29 @@ -file(GLOB obj_files "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/*.c") +get_current_directory_name(dir "${CMAKE_CURRENT_SOURCE_DIR}") +set(lib_name "${dir}_obj") +get_source_files(obj_files "${CMAKE_CURRENT_SOURCE_DIR}") add_library( - lib_obj + ${lib_name} OBJECT ${obj_files} ) -set(ALL_OBJECT_FILES ${ALL_OBJECT_FILES} $ PARENT_SCOPE) \ No newline at end of file +set(ALL_OBJECT_FILES ${ALL_OBJECT_FILES} $ PARENT_SCOPE) +set(ALL_OBJECTS ${ALL_OBJECTS} $ PARENT_SCOPE) + +get_test_files(test_files "${CMAKE_CURRENT_SOURCE_DIR}") +if(test_files) + include(3rdParty) + include_googletest() + set(test_name "${dir}_unit_test") + add_executable(${test_name} EXCLUDE_FROM_ALL ${test_files}) + target_link_libraries(${test_name} PRIVATE ${lib_name} gtest gmock_main) + + set_target_properties(${test_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${UnitTestsBinaryPath}") + + add_custom_target( + "unit-test-${dir}" + COMMENT "Running unit-test ..." + COMMAND ${test_name} + ) +endif(test_files) diff --git a/src/lib/lib.cpp b/src/lib/lib.cpp index da0e2f4..68fa6fb 100644 --- a/src/lib/lib.cpp +++ b/src/lib/lib.cpp @@ -1,9 +1,8 @@ #include "lib/lib.h" template -T add(T a, T b) -{ - return a + b; +T add(T a, T b) { + return a + b; } template int add(int a, int b); diff --git a/src/lib/lib.test.cpp b/src/lib/lib.test.cpp new file mode 100644 index 0000000..e68af2f --- /dev/null +++ b/src/lib/lib.test.cpp @@ -0,0 +1,13 @@ +#include "lib/lib.h" + +#include + +TEST(UnitTest, HandlesPositiveInput) { + EXPECT_EQ(add(1, 2), 3); + EXPECT_EQ(add(10, 20), 30); +} + +TEST(UnitTest, HandlesNegativeInput) { + EXPECT_EQ(add(-1, -2), -3); + EXPECT_EQ(add(-10, -20), -30); +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c547b9a..72de9bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,37 +1,34 @@ enable_testing() - -# TODO how to read from the local directory set(test_dir_list "${CMAKE_CURRENT_SOURCE_DIR}/lib") foreach(test_dir ${test_dir_list}) - file(GLOB_RECURSE test_files "${test_dir}/*-test.cpp" "${test_dir}/*-test.cc") - get_filename_component(dir_name ${test_dir} NAME) + file(GLOB_RECURSE test_files "${test_dir}/*.cpp" "${test_dir}/*.cc") + get_filename_component(dir_name ${test_dir} NAME_WE) set(test_name ${dir_name}_test) add_executable(${test_name} EXCLUDE_FROM_ALL ${test_files}) - target_link_libraries(${test_name} PRIVATE lib_obj gtest gmock_main) + target_link_libraries(${test_name} PRIVATE ${ALL_OBJECTS} gtest gmock_main) add_custom_target( test-${dir_name} COMMENT "Running test ${test_name}..." COMMAND ${test_name} ) - set_target_properties(${test_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") + set_target_properties(${test_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${TestsBinaryPath}") list(APPEND TEST_TARGETS ${test_name}) add_test(NAME ${test_name} COMMAND ${test_name}) endforeach(test_dir ${test_dir_list}) add_custom_target(run-tests - COMMAND ${CMAKE_CTEST_COMMAND} -C Debug + COMMAND ${CMAKE_CTEST_COMMAND} -C Release DEPENDS ${TEST_TARGETS} COMMENT "Build and Run All Tests..." ) add_custom_target(check-tests - COMMAND ${CMAKE_CTEST_COMMAND} -C Debug --verbose + COMMAND ${CMAKE_CTEST_COMMAND} -C Debug --verbose DEPENDS ${TEST_TARGETS} COMMENT "Runnig All Tests..." ) -