From 9687ee7fd1b1e0ca3e109bdae376e5ffaea8c46e Mon Sep 17 00:00:00 2001 From: Fraxy V Date: Sun, 1 Sep 2024 13:29:12 +0300 Subject: [PATCH] coverage action test --- .github/workflows/c-cpp.yml | 71 +++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 18 +++++++++- coverage.cmake | 37 +++++++++++++++++++ scripts/check-coverage.js | 31 ++++++++++++++++ scripts/diff.js | 36 +++++++++++++++++++ scripts/targets.js | 46 ++++++++++++++++++++++++ src/my_static_lib.cpp | 7 ++-- src/test1.cpp | 9 +++++ src/test2.cpp | 9 +++++ 9 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/c-cpp.yml create mode 100644 coverage.cmake create mode 100644 scripts/check-coverage.js create mode 100644 scripts/diff.js create mode 100644 scripts/targets.js create mode 100644 src/test1.cpp create mode 100644 src/test2.cpp diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml new file mode 100644 index 0000000..5ac1a1a --- /dev/null +++ b/.github/workflows/c-cpp.yml @@ -0,0 +1,71 @@ +name: Build +on: + push: +jobs: + build-project: + name: Build Project + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v4.1.7 + + - name: Fetch master + run: | + git fetch --no-tags --prune --depth=1 origin master + + - name: Setup Ninja + uses: seanmiddleditch/gha-setup-ninja@v5 + + - name: Build Project + uses: threeal/cmake-action@v2.0.0 + with: + generator: Ninja + options: | + CMAKE_EXPORT_COMPILE_COMMANDS=ON + ENABLE_COVERAGE=ON + run-build: false + + - name: Get modified source + id: get-modified-source + uses: actions/github-script@v7 + with: + script: | + const diff = require('./scripts/diff.js'); + return diff(); + + - name: Get affected targets + id: find-targets + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const targets = require('./scripts/targets.js'); + return targets('${{steps.get-modified-source.outputs.result}}'); + + - name: Build affected targets + if: ${{steps.find-targets.outputs.result != ''}} + run: ninja -C build ${{steps.find-targets.outputs.result}} + + - name: Run ctest + if: ${{steps.find-targets.outputs.result != ''}} + run: ctest --test-dir build + + - name: Generate a code coverage report + if: ${{steps.find-targets.outputs.result != ''}} + uses: threeal/gcovr-action@xml-out + with: + xml-out: coverage.xml + + - run: npm install xml2js + + - name: Check coverage report + id: check-coverage + uses: actions/github-script@v7 + with: + script: | + const check = require('./scripts/check-coverage.js'); + return await check('${{steps.get-modified-source.outputs.result}}', 'coverage.xml'); + + - name: Return coverage result + if: ${{steps.check-coverage.outputs.result == false}} + run: exit(1) diff --git a/CMakeLists.txt b/CMakeLists.txt index 69da88d..9731cc1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,25 @@ -project(coverage_action) +cmake_minimum_required(VERSION 3.22) +project(coverage_action) +include(CTest) add_library(my_static_lib STATIC src/my_static_lib.cpp) add_library(my_shared_lib SHARED src/my_shared_lib.cpp) +if (BUILD_TESTING) + enable_testing() + include(coverage.cmake) + add_executable(test_shared_lib src/test1.cpp) + target_link_libraries(test_shared_lib my_shared_lib) + + add_executable(test_static_lib src/test2.cpp) + target_link_libraries(test_static_lib my_static_lib) + + add_test(NAME test_shared_lib COMMAND test_shared_lib) + add_test(NAME test_static_lib COMMAND test_static_lib) +endif() + + add_executable(main src/main.cpp) target_link_libraries(main my_static_lib my_shared_lib) \ No newline at end of file diff --git a/coverage.cmake b/coverage.cmake new file mode 100644 index 0000000..9aaf37c --- /dev/null +++ b/coverage.cmake @@ -0,0 +1,37 @@ +#========================================================================= +# +# This software is distributed WITHOUT ANY WARRANTY; without even +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the above copyright notice for more information. +# +#========================================================================= + +# This code has been adapted from remus (https://gitlab.kitware.com/cmb/remus) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR + CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + set(CMAKE_COMPILER_IS_CLANGXX 1) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANGXX) + + include(CheckCXXCompilerFlag) + + #Add option for enabling gcov coverage + option(ENABLE_COVERAGE "Build with gcov support." OFF) + mark_as_advanced(ENABLE_COVERAGE) + + if(ENABLE_COVERAGE) + #We're setting the CXX flags and C flags beacuse they're propagated down + #independent of build type. + if(CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage -fno-elide-constructors -fprofile-instr-generate -fcoverage-mapping") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage -fprofile-instr-generate -fcoverage-mapping") + endif() + endif() +endif() diff --git a/scripts/check-coverage.js b/scripts/check-coverage.js new file mode 100644 index 0000000..332b37c --- /dev/null +++ b/scripts/check-coverage.js @@ -0,0 +1,31 @@ +module.exports = async (sources, covFile) => { + const fs = require('fs'); + const parseString = require('xml2js').parseString; + const result = await new Promise((resolve, reject) => parseString(fs.readFileSync(covFile, 'utf8'), (err, result) => { + if (err) reject(err); + else resolve(result); + })); + sources = JSON.parse(sources); + for (let packages of result.coverage.packages) { + for (let package of packages.package) { + let file = package.$.name; + for (let classes of package.classes) { + for (let clazz of classes.class) { + for (let lines of clazz.lines) { + for (let line of lines.line) { + if (line.$.hits === '0') { + const idx = sources.findIndex(source => source.file === clazz.$.filename && source.lines.includes(parseInt(line.$.number))); + if (idx !== -1) { + console.log(`Uncovered line in ${clazz.$.filename}:${line.$.number}`); + // Found an uncovered line in a changed file + return false; + } + } + } + } + } + } + } + } + return true; +}; \ No newline at end of file diff --git a/scripts/diff.js b/scripts/diff.js new file mode 100644 index 0000000..f26993b --- /dev/null +++ b/scripts/diff.js @@ -0,0 +1,36 @@ +const { execSync } = require('child_process'); +module.exports = () => { + const output = execSync(`git diff origin/master`).toString().split('\n') + let count = 0; + let start = 0; + let current = 0; + let sources = []; + for (let i = 0; i < output.length; i++) { + if (output[i].startsWith('diff --git')) { + // skip 'diff --git a/' prefix + const file = output[i].split(' ')[2].substring(2); + sources.push({'file': file, 'lines': []}); + // go to header line, e.g. '@@ -1,2 +1,3 @@' + do { + ++i; + } + while (!output[i].startsWith('@@')); + output[i].split(' ').forEach((c) => { + if (c.startsWith('+')) { + [start, count] = c.split(','); + start = parseInt(start); + count = parseInt(count); + current = 0; + } + }); + } + else if (current < count && !output[i].startsWith('-') && !output[i].startsWith('\\ No newline at end of file')) { + if (output[i].startsWith('+')) { + sources[sources.length - 1].lines.push(start + current); + } + ++current; + } + } + + return sources; +}; diff --git a/scripts/targets.js b/scripts/targets.js new file mode 100644 index 0000000..b549dc2 --- /dev/null +++ b/scripts/targets.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); + +module.exports = (sources) => { + let outputs = []; + sources = JSON.parse(sources); + let commands = JSON.parse(fs.readFileSync('build/compile_commands.json', 'utf8')); + for (let {file, _} of sources) { + if (file.endsWith('.cpp') || file.endsWith('.h') || file.endsWith('.c') + || file.endsWith('.hpp')) { + const idx = commands.findIndex(entry => entry.file.endsWith(file)); + if (idx >= 0) { + let entry = commands[idx]; + if (entry.output !== 'undefined') { + entry.output = entry.command.split(' ').find(token => token.endsWith('.o')); + } + outputs.push(entry.output); + commands.splice(idx, 1); + } + } + } + + targets = new Set(); + while (outputs.length > 0) { + level_targets = new Set(); + for (const output of outputs) { + let lines = execSync(`ninja -C build -t query ${output}`).toString().split('\n'); + + let insert = false; + for (const line of lines) { + if (line.endsWith('outputs:')) { + insert = true; + continue; + } + const value = line.trim(); + if (insert && value.length > 0) { + level_targets.add(value); + } + } + } + + outputs = [...level_targets]; + targets = new Set([...targets, ...level_targets]); + } + return [...targets].join(' '); +} \ No newline at end of file diff --git a/src/my_static_lib.cpp b/src/my_static_lib.cpp index dd9eaa2..cc4fca9 100644 --- a/src/my_static_lib.cpp +++ b/src/my_static_lib.cpp @@ -3,6 +3,7 @@ int method_in_static_lib() { return 42; } -int method_in_static_lib2() { - return 43; -} \ No newline at end of file +int method_in_static_lib3() { + int p = 65; + return p; +} diff --git a/src/test1.cpp b/src/test1.cpp new file mode 100644 index 0000000..9d07ee5 --- /dev/null +++ b/src/test1.cpp @@ -0,0 +1,9 @@ +int method_in_shared_lib(); + +int main() { + if (42 == method_in_shared_lib()) { + return 0; + } else { + return 1; + } +} \ No newline at end of file diff --git a/src/test2.cpp b/src/test2.cpp new file mode 100644 index 0000000..ab12b55 --- /dev/null +++ b/src/test2.cpp @@ -0,0 +1,9 @@ +int method_in_static_lib(); + +int main() { + if (42 == method_in_static_lib()) { + return 0; + } else { + return 1; + } +} \ No newline at end of file