diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c89b144..dfc65792 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,42 +11,161 @@ on: branches: [ master ] jobs: - # Job: Unit test suite - unit-tests: - name: "Unit Tests" - runs-on: ubuntu-latest + # Job: Ruby unit tests across all supported Ruby versions and OSes. + # These are fast (no C compilation) and verify the generator logic is Ruby-version-portable. + ruby-tests: + name: "Ruby Tests (${{ matrix.os }}, Ruby ${{ matrix.ruby }})" + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: + os: [ubuntu-latest, windows-latest, macos-latest] ruby: ['3.0', '3.1', '3.2', '3.3'] + exclude: + # create sparse matrix to avoid pointless duplication + - os: macos-latest + ruby: '3.0' + - os: macos-latest + ruby: '3.1' + - os: macos-latest + ruby: '3.2' + - os: windows-latest + ruby: '3.0' + - os: windows-latest + ruby: '3.1' + - os: windows-latest + ruby: '3.2' steps: - # Install Multilib + - name: Checkout Latest Repo + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + - name: Install Ruby Dependencies + run: | + gem install bundler + bundle install + + - name: Run Ruby Unit Tests + env: + NO_COLOR: ${{ matrix.os == 'windows-latest' && '1' || '' }} + run: | + cd test && rake test:unit + + - name: Run Style Check + env: + NO_COLOR: ${{ matrix.os == 'windows-latest' && '1' || '' }} + run: | + cd test && rake style:check + + # Job: C compilation and system tests — only needs to run on one Ruby version per OS, + # since the generated C code and runtime behavior don't vary with the Ruby version. + c-tests: + name: "C Tests (${{ matrix.os }})" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + # Install Multilib (Linux only) - name: Install Multilib + if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update -qq sudo apt-get install --assume-yes --quiet gcc-multilib - # Checks out repository under $GITHUB_WORKSPACE + # Add MinGW GCC to PATH (Windows only — MSYS2 is pre-installed on the runner) + - name: Add GCC to PATH + if: matrix.os == 'windows-latest' + run: echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Checkout Latest Repo - uses: actions/checkout@v2 - with: + uses: actions/checkout@v4 + with: submodules: recursive - # Setup Ruby Testing Tools to do tests on multiple ruby version - - name: Setup Ruby Testing Tools + - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby }} + ruby-version: '3.3' - # Install Ruby Testing Tools - - name: Setup Ruby Testing Tools + - name: Install Ruby Dependencies run: | - sudo gem install rspec - sudo gem install rubocop -v 1.57.2 - sudo gem install bundler - bundle update - bundle install + gem install bundler + bundle install + + - name: Run C Unit Tests + env: + NO_COLOR: ${{ matrix.os == 'windows-latest' && '1' || '' }} + run: cd test && rake test:c + + - name: Run System Tests + env: + NO_COLOR: ${{ matrix.os == 'windows-latest' && '1' || '' }} + run: cd test && rake test:system + + - name: Run Examples + env: + NO_COLOR: ${{ matrix.os == 'windows-latest' && '1' || '' }} + run: cd test && rake test:examples + + # Job: Valgrind memory-leak check (Linux/gcc_64 only, latest Ruby) + valgrind: + name: "Valgrind Memory Check" + runs-on: ubuntu-latest + steps: + - name: Install Dependencies + run: | + sudo apt-get update -qq + sudo apt-get install --assume-yes --quiet gcc-multilib valgrind + + - name: Checkout Latest Repo + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + + - name: Install Ruby Dependencies + run: | + gem install bundler + bundle install + + # Build and run C unit tests, then re-run the executable under valgrind. + # test:system clobbers the build directory, so check TestCMockC before that happens. + - name: Build and Run C Unit Tests + run: cd test && rake config[gcc_64_valgrind] test:c + + - name: Valgrind Check - C Unit Tests + run: | + valgrind --leak-check=full --track-origins=yes --error-exitcode=1 \ + test/system/build/TestCMockC.exe + + # Build and run system tests, then re-run each executable under valgrind. + - name: Build and Run System Tests + run: cd test && rake config[gcc_64_valgrind] test:system - # Run Tests - - name: Run All Unit Tests + - name: Valgrind Check - System Tests run: | - cd test && rake ci + failed=0 + for exe in test/system/build/test_*.exe; do + echo "Checking: $exe" + # Use exit code 42 to distinguish valgrind errors from Unity test failures. + # Some executables intentionally contain tests expected to fail (testing CMock's + # error-handling), so Unity exits non-zero. The `|| exit_code=$?` prevents bash's + # set -e from aborting the script on Unity's non-zero exit, while still capturing + # valgrind's own error exit code (42) separately. + exit_code=0 + valgrind --leak-check=full --track-origins=yes --error-exitcode=42 "$exe" || exit_code=$? + [ $exit_code -eq 42 ] && failed=1 + done + exit $failed diff --git a/Gemfile b/Gemfile index c6d4f853..899065c4 100644 --- a/Gemfile +++ b/Gemfile @@ -1 +1,4 @@ -source "http://rubygems.org/" +source "https://rubygems.org/" + +gem 'rspec' +gem 'rubocop', '1.57.2' diff --git a/docs/CMock_Summary.md b/docs/CMock_Summary.md index 6728b936..7e799dbc 100644 --- a/docs/CMock_Summary.md +++ b/docs/CMock_Summary.md @@ -189,7 +189,7 @@ care how many times it was called, right? StopIgnore: ------- -Maybe you want to ignore a particular function for part of a test but dont want to +Maybe you want to ignore a particular function for part of a test but don't want to ignore it later on. In that case, you want to use StopIgnore which will cancel the previously called Ignore or IgnoreAndReturn requiring you to Expect or otherwise handle the call to a function. @@ -199,6 +199,25 @@ handle the call to a function. * `retval func(void)` => `void func_StopIgnore(void)` * `retval func(params)` => `void func_StopIgnore(void)` +It's important to note that the effect of this function is immediate and applies to +this function's stack of expectations. So the following will work as intended: + +``` +Blah_Ignore(); +funcThatMightCallBlahButWeDoNotCare(); +Blah_StopIgnore(); +funcThatWeWantToMakeSureDoesNotCallBlah(); +``` + +But this is NOT going to work, because StopIgnore immediately cancels ignore: + +``` +Blah_Ignore(); +Blah_StopIgnore(); +funcThatMightCallBlahButWeDoNotCare(); +funcThatWeWantToMakeSureDoesNotCallBlah(); +``` + IgnoreStateless: ---------------- @@ -951,8 +970,9 @@ that exposes them as virtual methods and modify your code to inject mocks at run-time... but there is another way! Simply use CMock to mock the static member methods and a C++ mocking framework -to handle the virtual methods. (Yes, you can mix mocks from CMock and a C++ -mocking framework together in the same test!) +to handle the virtual methods. CMock does NOT mock non-static members. For those, +you'll need an actual C++ mocking framework. (Yes, you can mix mocks from CMock +and a C++ mocking framework together in the same test!) Keep in mind that since C++ mocking frameworks often link the real object to the unit test too, we need to resolve multiple definition errors with something like diff --git a/lib/cmock_generator_plugin_return_thru_ptr.rb b/lib/cmock_generator_plugin_return_thru_ptr.rb index 755ddbfa..85eb5f7b 100644 --- a/lib/cmock_generator_plugin_return_thru_ptr.rb +++ b/lib/cmock_generator_plugin_return_thru_ptr.rb @@ -80,12 +80,11 @@ def mock_precheck_return_thru_ptr(function) arg_name = arg[:name] next unless @utils.ptr_or_str?(arg[:type]) && !(arg[:const?]) - dest_cast = arg[:volatile?] ? '(void*)(CMOCK_MEM_PTR_AS_INT)' : '(void*)' lines << " if (Mock.#{function[:name]}_IgnoreBool && cmock_call_instance != NULL &&\n" lines << " cmock_call_instance->ReturnThruPtr_#{arg_name}_Used)\n" lines << " {\n" lines << " UNITY_TEST_ASSERT_NOT_NULL(#{arg_name}, cmock_line, CMockStringPtrIsNULL);\n" - lines << " CMOCK_MEMCPY(#{dest_cast}#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" + lines << " CMOCK_MEMCPY((void*)#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" lines << " cmock_call_instance->ReturnThruPtr_#{arg_name}_Size);\n" lines << " }\n" end @@ -136,11 +135,10 @@ def mock_implementation(function) arg_name = arg[:name] next unless @utils.ptr_or_str?(arg[:type]) && !(arg[:const?]) - dest_cast = arg[:volatile?] ? '(void*)(CMOCK_MEM_PTR_AS_INT)' : '(void*)' lines << " if (cmock_call_instance->ReturnThruPtr_#{arg_name}_Used)\n" lines << " {\n" lines << " UNITY_TEST_ASSERT_NOT_NULL(#{arg_name}, cmock_line, CMockStringPtrIsNULL);\n" - lines << " CMOCK_MEMCPY(#{dest_cast}#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" + lines << " CMOCK_MEMCPY((void*)#{arg_name}, (const void*)cmock_call_instance->ReturnThruPtr_#{arg_name}_Val,\n" lines << " cmock_call_instance->ReturnThruPtr_#{arg_name}_Size);\n" lines << " }\n" end diff --git a/lib/cmock_header_parser.rb b/lib/cmock_header_parser.rb index c3140031..dfc82847 100644 --- a/lib/cmock_header_parser.rb +++ b/lib/cmock_header_parser.rb @@ -15,7 +15,7 @@ def initialize(cfg) @c_calling_conventions = cfg.c_calling_conventions.uniq @treat_as_array = cfg.treat_as_array @treat_as_void = (['void'] + cfg.treat_as_void).uniq - @function_declaration_parse_base_match = '([\w\s\*\(\),\[\]]*?\w[\w\s\*\(\),\[\]]*?)\(([\w\s\*\(\),\.\[\]+\-\/]*)\)' + @function_declaration_parse_base_match = '([^(]*\w)\s*\(([\w\s\*\(\),\.\[\]+\-\/]*)\)' @declaration_parse_matcher = /#{@function_declaration_parse_base_match}$/m @standards = (%w[int short char long unsigned signed] + cfg.treat_as.keys).uniq @array_size_name = cfg.array_size_name @@ -118,15 +118,13 @@ def remove_comments_from_source(source) def remove_nested_pairs_of_braces(source) # remove nested pairs of braces because no function declarations will be inside of them (leave outer pair for function definition detection) - if RUBY_VERSION.split('.')[0].to_i > 1 - # we assign a string first because (no joke) if Ruby 1.9.3 sees this line as a regex, it will crash. - r = '\\{([^\\{\\}]*|\\g<0>)*\\}' - source.gsub!(/#{r}/m, '{ }') - else - while source.gsub!(/\{[^{}]*\{[^{}]*\}[^{}]*\}/m, '{ }') - end + # Collapse innermost brace pairs first using a brace-free sentinel (\x00), working + # outward until no balanced pairs remain. This avoids the catastrophic backtracking + # of the recursive regex \{([^\{\}]*|\g<0>)*\} on Ruby < 3.2 while preserving + # identical semantics: every balanced brace structure is collapsed to '{ }'. + while source.gsub!(/\{[^{}]*\}/m, "\x00") end - + source.gsub!("\x00", '{ }') source end @@ -311,7 +309,7 @@ def import_source(source, parse_project, cpp = false) source.gsub!(/\b(?:#{@ct_assert_patterns.join('|')})\s*\([^;]*\)/, '') unless @ct_assert_patterns.empty? # strip any remaining WORD(...==...) etc. -- calls containing comparison operators cannot be C function prototypes # must run before default-value removal, which would corrupt "!= 0" into "!" by removing "= 0" - source.gsub!(/\b\w+\s*\((?:[^()!=<>]*(?:\([^()]*\))*)*(?:==|!=|<=|>=)[^;]*\)/, '') + source.gsub!(/\b\w+\s*\((?:[^()!=<>]|\([^()]*\))*(?:==|!=|<=|>=)[^;]*\)/, '') source.gsub!(/\s*=\s*['"a-zA-Z0-9_.]+\s*/, '') # remove default value statements from argument lists diff --git a/test/gcc_64_valgrind.yml b/test/gcc_64_valgrind.yml new file mode 100644 index 00000000..a3af73b7 --- /dev/null +++ b/test/gcc_64_valgrind.yml @@ -0,0 +1,48 @@ +# ========================================================================= +# CMock - Automatic Mock Generation for C +# ThrowTheSwitch.org +# Copyright (c) 2007-26 Mike Karlesky, Mark VanderVoord, & Greg Williams +# SPDX-License-Identifier: MIT +# ========================================================================= + +# gcc_64 with debug symbols enabled for meaningful valgrind output. +# Used by the CI valgrind job to check for memory leaks and errors. + +--- +:tools: + :test_compiler: + :name: compiler + :executable: gcc + :arguments: + - "-c" + - "-m64" + - "-g" + - "-Wall" + - "-Wno-address" + - "-std=c99" + - "-pedantic" + - '-I"${5}"' + - "-D${6}" + - "${1}" + - "-o ${2}" + :test_linker: + :name: linker + :executable: gcc + :arguments: + - "${1}" + - "-lm" + - "-m64" + - "-o ${2}" +:extension: + :object: ".o" + :executable: ".exe" +:defines: + :test: + - UNITY_EXCLUDE_STDINT_H + - UNITY_EXCLUDE_LIMITS_H + - UNITY_INCLUDE_DOUBLE + - UNITY_SUPPORT_TEST_CASES + - UNITY_SUPPORT_64 + - UNITY_INT_WIDTH=32 + - UNITY_LONG_WIDTH=64 + - UNITY_POINTER_WIDTH=64 diff --git a/test/rakefile_helper.rb b/test/rakefile_helper.rb index 66030ac9..8c9adbab 100644 --- a/test/rakefile_helper.rb +++ b/test/rakefile_helper.rb @@ -80,7 +80,7 @@ def load_configuration(config_file, cmock_overlay = nil) raise "Cannot find Config File #{config_target}" end - $colour_output = $proj[:project][:colour] + $colour_output = $proj[:project][:colour] && !ENV['NO_COLOR'] end def configure_clean diff --git a/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb b/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb index 74055594..61852bc4 100644 --- a/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb +++ b/test/unit/cmock_generator_plugin_return_thru_ptr_test.rb @@ -253,14 +253,14 @@ def volatile_ptr_func_expect assert_equal(expected, returned) end - it "uses (void*)(CMOCK_MEM_PTR_AS_INT) cast in mock_implementation for volatile pointer arg" do + it "uses (void*) cast in mock_implementation for volatile pointer arg" do volatile_ptr_func_expect() expected = " if (cmock_call_instance->ReturnThruPtr_foo_handle_Used)\n" + " {\n" + " UNITY_TEST_ASSERT_NOT_NULL(foo_handle, cmock_line, CMockStringPtrIsNULL);\n" + - " CMOCK_MEMCPY((void*)(CMOCK_MEM_PTR_AS_INT)foo_handle, (const void*)cmock_call_instance->ReturnThruPtr_foo_handle_Val,\n" + + " CMOCK_MEMCPY((void*)foo_handle, (const void*)cmock_call_instance->ReturnThruPtr_foo_handle_Val,\n" + " cmock_call_instance->ReturnThruPtr_foo_handle_Size);\n" + " }\n"