├── integrtest ├── load.py ├── includes │ ├── add.h │ ├── gpio.h │ ├── gpio_lib.h │ ├── complex.h │ └── types.h ├── main.c ├── add.c ├── Makefile ├── complex.c ├── gpio.c └── test.py ├── unittest ├── load.py ├── add.h ├── gpio.h ├── sum.h ├── gpio_lib.h ├── complex.h ├── add.c ├── types.h ├── complex.c ├── sum.c ├── gpio.c ├── Makefile ├── test_add.py ├── test_complex.py ├── test_sum.py └── test_gpio.py ├── .gitignore ├── .pre-commit-config.yaml ├── Makefile ├── LICENSE ├── CONTRIBUTING ├── py_loaded_gcovr.sh ├── .clang-format ├── doc └── Troubleshooting.md ├── README.md └── load.py /integrtest/load.py: -------------------------------------------------------------------------------- 1 | ../load.py -------------------------------------------------------------------------------- /unittest/load.py: -------------------------------------------------------------------------------- 1 | ../load.py -------------------------------------------------------------------------------- /unittest/add.h: -------------------------------------------------------------------------------- 1 | int add(int a, int b); 2 | -------------------------------------------------------------------------------- /unittest/gpio.h: -------------------------------------------------------------------------------- 1 | int read_gpio(int number); 2 | -------------------------------------------------------------------------------- /integrtest/includes/add.h: -------------------------------------------------------------------------------- 1 | int add(int a, int b); 2 | -------------------------------------------------------------------------------- /integrtest/includes/gpio.h: -------------------------------------------------------------------------------- 1 | int read_gpio(int number); 2 | -------------------------------------------------------------------------------- /unittest/sum.h: -------------------------------------------------------------------------------- 1 | void sum_reset(void); 2 | int sum(int a); 3 | -------------------------------------------------------------------------------- /unittest/gpio_lib.h: -------------------------------------------------------------------------------- 1 | int read_gpio0(void); 2 | int read_gpio1(void); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | __pycache__/ 3 | *.pyc 4 | *.o 5 | *_.c 6 | *_.*.so 7 | -------------------------------------------------------------------------------- /integrtest/includes/gpio_lib.h: -------------------------------------------------------------------------------- 1 | int read_gpio0(void); 2 | int read_gpio1(void); 3 | -------------------------------------------------------------------------------- /integrtest/main.c: -------------------------------------------------------------------------------- 1 | int main(int argc, char **argv) { 2 | return 0; 3 | } 4 | -------------------------------------------------------------------------------- /unittest/complex.h: -------------------------------------------------------------------------------- 1 | #include "types.h" 2 | 3 | complex add_complex(complex a, complex b); 4 | -------------------------------------------------------------------------------- /integrtest/add.c: -------------------------------------------------------------------------------- 1 | #include "add.h" 2 | 3 | int add(int a, int b) { 4 | return a + b; 5 | } 6 | -------------------------------------------------------------------------------- /unittest/add.c: -------------------------------------------------------------------------------- 1 | #include "add.h" 2 | 3 | int add(int a, int b) { 4 | return a + b; 5 | } 6 | -------------------------------------------------------------------------------- /unittest/types.h: -------------------------------------------------------------------------------- 1 | typedef struct { 2 | float real; 3 | float imaginary; 4 | } complex; 5 | -------------------------------------------------------------------------------- /integrtest/includes/complex.h: -------------------------------------------------------------------------------- 1 | #include "types.h" 2 | 3 | complex complex_add(complex a, complex b); 4 | -------------------------------------------------------------------------------- /integrtest/includes/types.h: -------------------------------------------------------------------------------- 1 | typedef struct { 2 | float real; 3 | float imaginary; 4 | } complex; 5 | -------------------------------------------------------------------------------- /integrtest/Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | test: 4 | python3 -m unittest 5 | 6 | clean: 7 | rm -fr *.o *.so *_.c *.pyc __pycache__/ 8 | -------------------------------------------------------------------------------- /unittest/complex.c: -------------------------------------------------------------------------------- 1 | #include "complex.h" 2 | 3 | complex add_complex(complex a, complex b) { 4 | a.real += b.real; 5 | a.imaginary += b.imaginary; 6 | return a; 7 | } 8 | -------------------------------------------------------------------------------- /integrtest/complex.c: -------------------------------------------------------------------------------- 1 | #include "complex.h" 2 | 3 | complex complex_add(complex a, complex b) { 4 | a.real += b.real; 5 | a.imaginary += b.imaginary; 6 | return a; 7 | } 8 | -------------------------------------------------------------------------------- /unittest/sum.c: -------------------------------------------------------------------------------- 1 | #include "sum.h" 2 | 3 | static int _sum = 0; 4 | 5 | void sum_reset(void) { 6 | _sum = 0; 7 | } 8 | 9 | int sum(int a) { 10 | _sum += a; 11 | return _sum; 12 | } 13 | -------------------------------------------------------------------------------- /integrtest/gpio.c: -------------------------------------------------------------------------------- 1 | #include "gpio.h" 2 | #include "gpio_lib.h" 3 | 4 | int read_gpio(int number) { 5 | switch (number) { 6 | case 0: 7 | return read_gpio0(); 8 | case 1: 9 | return read_gpio1(); 10 | default: 11 | return -1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /unittest/gpio.c: -------------------------------------------------------------------------------- 1 | #include "gpio.h" 2 | #include "gpio_lib.h" 3 | 4 | int read_gpio(int number) { 5 | switch (number) { 6 | case 0: 7 | return read_gpio0(); 8 | case 1: 9 | return read_gpio1(); 10 | default: 11 | return -1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /unittest/Makefile: -------------------------------------------------------------------------------- 1 | all: add.o sum.o complex.o gpio.o test 2 | 3 | test: 4 | python3 -m unittest 5 | 6 | gpio.o: gpio.c gpio.h gpio_lib.h 7 | gcc -c -o $@ $< 8 | 9 | complex.o: complex.c complex.h types.h 10 | gcc -c -o $@ $< 11 | 12 | %.o: %.c %.h 13 | gcc -c -o $@ $< 14 | 15 | clean: 16 | rm -fr *.o *.so *_.c *.pyc __pycache__/ 17 | -------------------------------------------------------------------------------- /unittest/test_add.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import unittest 3 | from load import load 4 | 5 | module, ffi = load("add.c", module_name="add_") 6 | 7 | 8 | class AddTest(unittest.TestCase): 9 | def test_addition(self): 10 | # Function name 'add' from created module 11 | self.assertEqual(module.add(1, 2), 1 + 2) 12 | 13 | 14 | if __name__ == "__main__": 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /unittest/test_complex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import unittest 3 | from load import load 4 | 5 | module, ffi = load("complex.c", module_name="complex_") 6 | 7 | 8 | class ComplexTest(unittest.TestCase): 9 | def test_addition(self): 10 | result = module.add_complex([0, 1], [2, 3]) 11 | self.assertEqual(result.real, 2) 12 | self.assertEqual(result.imaginary, 4) 13 | 14 | 15 | if __name__ == "__main__": 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-clang-format 3 | rev: v13.0.0 4 | hooks: 5 | - id: clang-format 6 | - repo: https://github.com/psf/black 7 | rev: 21.12b0 8 | hooks: 9 | - id: black 10 | args: [--line-length=80, --quiet] 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v4.0.1 13 | hooks: 14 | - id: check-yaml 15 | - id: mixed-line-ending 16 | args: [--fix=lf] 17 | - id: end-of-file-fixer 18 | - id: trailing-whitespace 19 | -------------------------------------------------------------------------------- /unittest/test_sum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import unittest 3 | from load import load 4 | 5 | module, ffi = load("sum.c", module_name="sum_") 6 | 7 | 8 | class SumTest(unittest.TestCase): 9 | def setUp(self): 10 | module.sum_reset() 11 | 12 | def test_zero(self): 13 | self.assertEqual(module.sum(0), 0) 14 | 15 | def test_one(self): 16 | self.assertEqual(module.sum(1), 1) 17 | 18 | def test_multiple(self): 19 | self.assertEqual(module.sum(2), 2) 20 | self.assertEqual(module.sum(4), 2 + 4) 21 | 22 | 23 | if __name__ == "__main__": 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /unittest/test_gpio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import unittest 3 | import unittest.mock 4 | from load import load 5 | 6 | module, ffi = load("gpio.c", module_name="gpio_") 7 | 8 | 9 | class GPIOTest(unittest.TestCase): 10 | def test_read_gpio0(self): 11 | @ffi.def_extern() 12 | def read_gpio0(): 13 | return 42 14 | 15 | self.assertEqual(module.read_gpio(0), 42) 16 | 17 | def test_read_gpio1(self): 18 | read_gpio1 = unittest.mock.MagicMock(return_value=21) 19 | ffi.def_extern("read_gpio1")(read_gpio1) 20 | self.assertEqual(module.read_gpio(1), 21) 21 | read_gpio1.assert_called_once_with() 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python3 2 | PYTHONPATH = PYTHONPATH= 3 | 4 | PYLINT = pylint 5 | PYLINT_OPTS = --jobs=0 6 | PYLINT_OPTS += --disable=missing-docstring 7 | PYLINT_OPTS += --disable=invalid-name 8 | PYLINT_OPTS += --disable=duplicate-code 9 | PYLINT_OPTS += --disable=fixme 10 | 11 | MYPY = mypy 12 | MYPYPATH = MYPYPATH= 13 | 14 | all: examples mypy pylint 15 | 16 | examples: 17 | cd unittest && make all 18 | cd integrtest && make all 19 | 20 | clean: 21 | @echo ">>> Cleaning..." 22 | find .. -name __pycache__ | xargs rm -fr 23 | find .. -name .mypy_cache | xargs rm -fr 24 | cd unittest && make clean 25 | cd integrtest && make clean 26 | 27 | mypy: 28 | @echo ">>> Versions..." 29 | $(PYTHON) --version 30 | $(MYPY) --version 31 | 32 | @echo ">>> Type hints check..." 33 | $(MYPYPATH) $(MYPY) load.py --ignore-missing-imports 34 | 35 | pylint: 36 | @echo ">>> Versions..." 37 | $(PYTHON) --version 38 | $(PYLINT) --version 39 | 40 | @echo ">>> Linting..." 41 | $(PYTHONPATH) $(PYLINT) $(PYLINT_OPTS) load.py 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Djones A. Boni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /integrtest/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import unittest 3 | import unittest.mock 4 | from load import load 5 | 6 | module_name = "pysim_" 7 | 8 | source_files = ["add.c", "complex.c", "gpio.c", "main.c"] 9 | 10 | include_paths = [ 11 | ".", 12 | "./includes", 13 | ] 14 | 15 | compiler_options = [ 16 | "-std=c90", 17 | "-pedantic", 18 | ] 19 | 20 | module, ffi = load( 21 | source_files, include_paths, compiler_options, module_name=module_name 22 | ) 23 | 24 | 25 | class AddTest(unittest.TestCase): 26 | def test_addition(self): 27 | self.assertEqual(module.add(1, 2), 1 + 2) 28 | 29 | 30 | class ComplexTest(unittest.TestCase): 31 | def test_addition(self): 32 | result = module.complex_add([0, 1], [2, 3]) 33 | self.assertEqual(result.real, 2) 34 | self.assertEqual(result.imaginary, 4) 35 | 36 | 37 | class GPIOTest(unittest.TestCase): 38 | def test_read_gpio0(self): 39 | @ffi.def_extern() 40 | def read_gpio0(): 41 | return 42 42 | 43 | self.assertEqual(module.read_gpio(0), 42) 44 | 45 | def test_read_gpio1(self): 46 | read_gpio1 = unittest.mock.MagicMock(return_value=21) 47 | ffi.def_extern("read_gpio1")(read_gpio1) 48 | self.assertEqual(module.read_gpio(1), 21) 49 | read_gpio1.assert_called_once_with() 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this 7 | license document, but changing it is not allowed. 8 | 9 | 10 | Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. 35 | -------------------------------------------------------------------------------- /py_loaded_gcovr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GCOVR_ARGS=() 4 | CONTEXT=3 5 | NO_CODE= 6 | NUM_CODE= 7 | 8 | while [ $# -ne 0 ]; do 9 | case "$1" in 10 | --no-code) 11 | NO_CODE=1 12 | shift 13 | ;; 14 | --num-code) 15 | NUM_CODE="$2" 16 | shift 2 17 | ;; 18 | --context) 19 | CONTEXT="$2" 20 | shift 2 21 | ;; 22 | *) 23 | GCOVR_ARGS[${#GCOVR_ARGS}]="$1" 24 | shift 25 | ;; 26 | esac 27 | done 28 | 29 | NUM=0 30 | 31 | gcovr "${GCOVR_ARGS[@]}" | while read Line; do 32 | # Get file name 33 | File=$(echo $Line | cut -f1 -d' ') 34 | 35 | # The file must exist 36 | [ ! -f "$File" ] && continue 37 | 38 | # Find start and end of actual code, not CFFI prefix and postfix 39 | Lines=$(grep -nE '/\*{60}/' "$File" | cut -f1 -d':') 40 | Start=$(echo $Lines | cut -f1 -d' ') 41 | End=$(echo $Lines | cut -f2 -d' ') 42 | 43 | # Get the lines 44 | Lines=$(echo $Line | cut -f5 -d' ' | tr ',' ' ') 45 | 46 | if [ ! -z $NO_CODE ]; then 47 | echo -n "$File: " 48 | fi 49 | 50 | # For each range 51 | for Range in $Lines; do 52 | # Begin and end of range 53 | A=$(echo $Range | cut -f1 -d'-' ) 54 | B=$(echo $Range | cut -f2 -d'-' ) 55 | 56 | # If out of actual code go to next 57 | [ $A -lt $Start ] && continue 58 | [ ! -z $B ] && { [ $B -lt $Start ] && continue; } 59 | [ $A -gt $End ] && continue 60 | [ ! -z $B ] && { [ $B -gt $End ] && continue; } 61 | 62 | AA=$((A - CONTEXT)) 63 | AX=$((A - 1)) 64 | BX=$((B + 1)) 65 | BB=$((B + CONTEXT)) 66 | 67 | # Print lines 68 | if [ -z $NO_CODE ]; then 69 | 70 | if [ ! -z "$NUM_CODE" ] && [ "$NUM" -ge "$NUM_CODE" ]; then 71 | break 72 | fi 73 | NUM=$((NUM + 1)) 74 | 75 | if [ "$CONTEXT" -gt 0 ]; then 76 | if [ $A -eq $B ]; then 77 | echo "$File:$A:" 78 | else 79 | echo "$File:$A-$B:" 80 | fi 81 | 82 | sed -n "s/^\(.*\)$/| \1/;${AA},${AX}p;" $File 83 | sed -n "s/^\(.*\)$/|> \1/;${A},${B}p;" $File 84 | sed -n "s/^\(.*\)$/| \1/;${BX},${BB}p;" $File 85 | 86 | echo 87 | else 88 | if [ $A -eq $B ]; then 89 | echo -n "$File:$A:" 90 | sed -n "s/^\s*\(.*\)$/ \1/;${A},${B}p;" $File 91 | else 92 | echo "$File:$A-$B:" 93 | sed -n "s/^\(.*\)$/ \1/;${A},${B}p;" $File 94 | fi 95 | fi 96 | fi 97 | 98 | if [ ! -z $NO_CODE ]; then 99 | if [ $A -eq $B ]; then 100 | echo -n "$A," 101 | else 102 | echo -n "$A-$B," 103 | fi 104 | 105 | fi 106 | done 107 | 108 | if [ ! -z $NO_CODE ]; then 109 | echo 110 | fi 111 | done 112 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Right 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: Inline 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: Never 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: MultiLine 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Attach 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 80 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: true 60 | DeriveLineEnding: true 61 | DerivePointerAlignment: false 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Preserve 70 | IncludeCategories: 71 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 72 | Priority: 2 73 | SortPriority: 0 74 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 75 | Priority: 3 76 | SortPriority: 0 77 | - Regex: '.*' 78 | Priority: 1 79 | SortPriority: 0 80 | IncludeIsMainRegex: '(Test)?$' 81 | IncludeIsMainSourceRegex: '' 82 | IndentCaseLabels: false 83 | IndentGotoLabels: true 84 | IndentPPDirectives: BeforeHash 85 | IndentWidth: 4 86 | IndentWrappedFunctionNames: false 87 | JavaScriptQuotes: Leave 88 | JavaScriptWrapImports: true 89 | KeepEmptyLinesAtTheStartOfBlocks: false 90 | MacroBlockBegin: '' 91 | MacroBlockEnd: '' 92 | MaxEmptyLinesToKeep: 1 93 | NamespaceIndentation: None 94 | ObjCBinPackProtocolList: Auto 95 | ObjCBlockIndentWidth: 2 96 | ObjCSpaceAfterProperty: false 97 | ObjCSpaceBeforeProtocolList: true 98 | PenaltyBreakAssignment: 2 99 | PenaltyBreakBeforeFirstCallParameter: 19 100 | PenaltyBreakComment: 300 101 | PenaltyBreakFirstLessLess: 120 102 | PenaltyBreakString: 1000 103 | PenaltyBreakTemplateDeclaration: 10 104 | PenaltyExcessCharacter: 1000000 105 | PenaltyReturnTypeOnItsOwnLine: 60 106 | PointerAlignment: Right 107 | ReflowComments: true 108 | SortIncludes: true 109 | SortUsingDeclarations: true 110 | SpaceAfterCStyleCast: false 111 | SpaceAfterLogicalNot: false 112 | SpaceAfterTemplateKeyword: true 113 | SpaceBeforeAssignmentOperators: true 114 | SpaceBeforeCpp11BracedList: false 115 | SpaceBeforeCtorInitializerColon: true 116 | SpaceBeforeInheritanceColon: true 117 | SpaceBeforeParens: ControlStatements 118 | SpaceBeforeRangeBasedForLoopColon: true 119 | SpaceInEmptyBlock: false 120 | SpaceInEmptyParentheses: false 121 | SpacesBeforeTrailingComments: 1 122 | SpacesInAngles: false 123 | SpacesInConditionalStatement: false 124 | SpacesInContainerLiterals: true 125 | SpacesInCStyleCastParentheses: false 126 | SpacesInParentheses: false 127 | SpacesInSquareBrackets: false 128 | SpaceBeforeSquareBrackets: false 129 | Standard: Latest 130 | StatementMacros: 131 | - Q_UNUSED 132 | - QT_REQUIRE_VERSION 133 | TabWidth: 4 134 | UseCRLF: false 135 | UseTab: Never 136 | ... 137 | -------------------------------------------------------------------------------- /doc/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Where is `main()`? 4 | 5 | The function `main()` is renamed to `mpmain()`, so it does not conflict with Python's `main()` function. Just use `module.mpmain(args)`. 6 | 7 | ## How to reset global variables? 8 | 9 | To reset global variables you have two options: 10 | 11 | - Create reset function (in C or in Python) and call it in `setUp()`. 12 | - Reload module with parameter `avoid_cache=True` in `setUp()`. This will recompile the module with a random name and avoid Python module caching. 13 | 14 | There may be other options, however. 15 | 16 | ## Static local variables with the same name 17 | 18 | You cannot use two "file-scoped" static variables with the same name. When creating the Python module, all the C souce code is accumulated into one single C file. Therefore you get a redefinition error. 19 | 20 | This will cause a redefinition error: 21 | 22 | ```c 23 | /* File: file1.c */ 24 | static uint32_t static_variable; 25 | 26 | /* File: file2.c */ 27 | static uint32_t static_variable; 28 | ``` 29 | 30 | This is OK: 31 | 32 | ```c 33 | /* File: file1.c */ 34 | static uint32_t file1_static_variable; 35 | 36 | /* File: file2.c */ 37 | static uint32_t file2_static_variable; 38 | ``` 39 | 40 | ## Function redefined 41 | 42 | This an example of an error related to a function redefinition: 43 | 44 | ``` 45 | pysim_.c:1997:6: error: redefinition of ‘FunctionName’ 46 | void FunctionName(int a0) 47 | ^~~~~~~~~ 48 | pysim_.c:947:6: note: previous definition of ‘FunctionName’ was here 49 | void FunctionName(int num) { 50 | ^~~~~~~~~ 51 | Traceback (most recent call last): 52 | File "/usr/lib/python3.6/distutils/unixccompiler.py", line 118, in _compile 53 | extra_postargs) 54 | File "/usr/lib/python3.6/distutils/ccompiler.py", line 909, in spawn 55 | spawn(cmd, dry_run=self.dry_run) 56 | File "/usr/lib/python3.6/distutils/spawn.py", line 36, in spawn 57 | _spawn_posix(cmd, search_path, dry_run=dry_run) 58 | File "/usr/lib/python3.6/distutils/spawn.py", line 159, in _spawn_posix 59 | % (cmd, exit_status)) 60 | distutils.errors.DistutilsExecError: command 'x86_64-linux-gnu-gcc' failed with exit status 1 61 | 62 | During handling of the above exception, another exception occurred: 63 | 64 | Traceback (most recent call last): 65 | File "/usr/lib/python3/dist-packages/cffi/ffiplatform.py", line 51, in _build 66 | dist.run_command('build_ext') 67 | File "/usr/lib/python3.6/distutils/dist.py", line 974, in run_command 68 | cmd_obj.run() 69 | File "/usr/lib/python3.6/distutils/command/build_ext.py", line 339, in run 70 | self.build_extensions() 71 | File "/usr/lib/python3.6/distutils/command/build_ext.py", line 448, in build_extensions 72 | self._build_extensions_serial() 73 | File "/usr/lib/python3.6/distutils/command/build_ext.py", line 473, in _build_extensions_serial 74 | self.build_extension(ext) 75 | File "/usr/lib/python3.6/distutils/command/build_ext.py", line 533, in build_extension 76 | depends=ext.depends) 77 | File "/usr/lib/python3.6/distutils/ccompiler.py", line 574, in compile 78 | self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) 79 | File "/usr/lib/python3.6/distutils/unixccompiler.py", line 120, in _compile 80 | raise CompileError(msg) 81 | distutils.errors.CompileError: command 'x86_64-linux-gnu-gcc' failed with exit status 1 82 | 83 | During handling of the above exception, another exception occurred: 84 | 85 | Traceback (most recent call last): 86 | File "test_librertos.py", line 29, in 87 | module, ffi = load(source_files, include_paths, compiler_options, module_name=module_name) 88 | File ".../PATH/load_c.py", line 118, in load 89 | ffibuilder.compile() 90 | File "/usr/lib/python3/dist-packages/cffi/api.py", line 697, in compile 91 | compiler_verbose=verbose, debug=debug, **kwds) 92 | File "/usr/lib/python3/dist-packages/cffi/recompiler.py", line 1520, in recompile 93 | compiler_verbose, debug) 94 | File "/usr/lib/python3/dist-packages/cffi/ffiplatform.py", line 22, in compile 95 | outputfilename = _build(tmpdir, ext, compiler_verbose, debug) 96 | File "/usr/lib/python3/dist-packages/cffi/ffiplatform.py", line 58, in _build 97 | raise VerificationError('%s: %s' % (e.__class__.__name__, e)) 98 | cffi.error.VerificationError: CompileError: command 'x86_64-linux-gnu-gcc' failed with exit status 1 99 | ``` 100 | 101 | The problem is likely to be in `visit_Decl()`: your function definition is not being found in the code by the regex. You would need to change your function definition name or the regex. 102 | 103 | This is an example of a change that was made to allow pointers to have spaces after the `*`, for example, `int * i_ptr`: 104 | 105 | ```python 106 | # File: load_c.py 107 | 108 | # From 109 | re.search(re.escape(result).replace('\\ ', '\\s*'), self.source_content) 110 | 111 | # To 112 | re.search(re.escape(result).replace('\\*', '\\*\\s*').replace('\\ ', '\\s*'), self.source_content) 113 | ``` 114 | 115 | ## Cannot parse header 116 | 117 | This an example of an error related to header that could not be parsed: 118 | 119 | ``` 120 | Traceback (most recent call last): 121 | File "test_librertos.py", line 29, in 122 | module, ffi = load(source_files, include_paths, compiler_options, module_name=module_name) 123 | File ".../PATH/load_c.py", line 84, in load 124 | ast_header = pycparser.CParser().parse(header_content) 125 | File ".../PATH/c_parser.py", line 152, in parse 126 | debug=debuglevel) 127 | File ".../PATH/ply/yacc.py", line 331, in parse 128 | return self.parseopt_notrack(input, lexer, debug, tracking, tokenfunc) 129 | File ".../PATH/ply/yacc.py", line 1199, in parseopt_notrack 130 | tok = call_errorfunc(self.errorfunc, errtoken, self) 131 | File ".../PATH/ply/yacc.py", line 193, in call_errorfunc 132 | r = errorfunc(token) 133 | File ".../PATH/c_parser.py", line 1861, in p_error 134 | column=self.clex.find_tok_column(p))) 135 | File ".../PATH/plyparser.py", line 67, in _parse_error 136 | raise ParseError("%s: %s" % (coord, msg)) 137 | pycparser.plyparser.ParseError: :258:39: before: __dest 138 | ``` 139 | 140 | The problem is likely to be a header (or source file) with directives that CFFI does not understand, such as `____attribute__()` or `__restrict`. Define the directive to the `Remove_Unknowns` variable to infor the C preprocessor to remove it. 141 | 142 | This is an example of a change that was made to allow inclusion of the `string.h` header, which uses the `__restrict` directive: 143 | 144 | ```python 145 | # File: load_c.py 146 | 147 | # From 148 | Remove_Unknowns = """\ 149 | #define __attribute__(x) 150 | """ 151 | 152 | # To 153 | Remove_Unknowns = """\ 154 | #define __attribute__(x) 155 | #define __restrict 156 | """ 157 | ``` 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unit-Test C with Python 2 | 3 | This project allows you to import normal C source code into a Python as a module. 4 | 5 | The function `load()`, defined in `load_c.py`, does all the work of creating the module from the source code. 6 | 7 | The source code and the headers used within them are processed with CFFI, compiled with GCC, and loaded as a Python module. 8 | 9 | ``` 10 | \ 11 | source/*.c | 12 | |o => CFFI => GCC => Python 13 | includes/*.h | 14 | / 15 | ``` 16 | 17 | With that you can do lots of cool things with, such as: 18 | 19 | * Create unit-tests of single C source file. 20 | * Create integration-test of multiple C source files. 21 | * Test embedded C code 22 | 23 | ## Quick example 24 | 25 | Testing an add function. 26 | 27 | ```c 28 | /* File: add.h */ 29 | 30 | int add(int a, int b); 31 | ``` 32 | 33 | ```c 34 | /* File: add.c */ 35 | 36 | #include "add.h" 37 | 38 | int add(int a, int b) 39 | { 40 | return a + b; 41 | } 42 | ``` 43 | 44 | ```python 45 | # File: test_add.py 46 | 47 | from load_c import load 48 | import unittest 49 | 50 | module, ffi = load('add.c') 51 | 52 | class AddTest(unittest.TestCase): 53 | 54 | def testAddtion(self): 55 | self.assertEqual(module.add(1, 2), 1 + 2) 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | ``` 60 | 61 | ## Another example 62 | 63 | Testing a semaphore implementation. 64 | 65 | ```python 66 | # File: test_semaphore.py 67 | 68 | from load_c import load 69 | import unittest 70 | 71 | source_files = [ 72 | 'semaphore.c', 73 | 'other_file.c', 74 | ] 75 | 76 | include_paths = [ 77 | '.', 78 | './includes', 79 | ] 80 | 81 | compiler_options = [ 82 | '-std=c90', 83 | '-pedantic', 84 | ] 85 | 86 | module, ffi = load(source_files, include_paths, compiler_options) 87 | 88 | class SemaphoreInit(unittest.TestCase): 89 | 90 | def setUp(self): 91 | self.psem = ffi.new("struct Semaphore_t[1]") 92 | self.sem = self.psem[0] 93 | 94 | def testInitBinary(self): 95 | count, max_ = 1, 1 96 | module.Semaphore_init(self.psem, count, max_) 97 | # Count and Max OK 98 | self.assertEqual(self.sem.Count, count) 99 | self.assertEqual(self.sem.Max, max_) 100 | 101 | def testInitCounter(self): 102 | count, max_ = 3, 5 103 | module.Semaphore_init(self.psem, count, max_) 104 | # Count and Max OK 105 | self.assertEqual(self.sem.Count, count) 106 | self.assertEqual(self.sem.Max, max_) 107 | 108 | if __name__ == '__main__': 109 | unittest.main() 110 | ``` 111 | 112 | ## Testing embedded C code 113 | 114 | To be able to test your embedded C code with Python (unit-test or integration test), the C code must be modular with respect to the hardware interface (or HAL - Hardware Abstraction Layer). 115 | 116 | Python functions and constructs will take HAL's place, providing similar functionality or even made out values to be processed by the functions under test. 117 | 118 | ``` 119 | +-----------+ +-----------+ 120 | |Application| \ |Application| 121 | |-----------| +---\ |-----------| 122 | | HAL | | ) | Python | 123 | |-----------| +---/ |-----------| 124 | | Hardware | / | Simulated | 125 | +-----------+ |peripherals| 126 | +-----------+ 127 | ``` 128 | 129 | ## Example of mocking hardware dependent calls 130 | 131 | Here is an example: the library exposed on gpio_lib.h has no source code available. We test if `read_gpio(int)` uses the correct calls to `read_gpio0()` and `read_gpio1()` using mock functions. 132 | 133 | ```c 134 | /* File: gpio_lib.h */ 135 | 136 | int read_gpio0(void); 137 | int read_gpio1(void); 138 | ``` 139 | 140 | ```c 141 | /* File: gpio.h */ 142 | 143 | int read_gpio(int number); 144 | ``` 145 | 146 | ```c 147 | /* File: gpio.c */ 148 | 149 | #include "gpio.h" 150 | #include "gpio_lib.h" 151 | 152 | int read_gpio(int number) 153 | { 154 | switch(number) 155 | { 156 | case 0: 157 | return read_gpio0(); 158 | case 1: 159 | return read_gpio1(); 160 | default: 161 | return -1; 162 | } 163 | } 164 | ``` 165 | 166 | ```python 167 | # File: test_gpio.py 168 | 169 | from load_c import load 170 | import unittest, unittest.mock 171 | 172 | source_files = [ 173 | 'gpio.c', 174 | ] 175 | 176 | include_paths = [ 177 | '.', 178 | './includes', 179 | ] 180 | 181 | compiler_options = [ 182 | '-std=c90', 183 | '-pedantic', 184 | ] 185 | 186 | module, ffi = load(source_files, include_paths, compiler_options) 187 | 188 | class GPIOTest(unittest.TestCase): 189 | 190 | def test_read_gpio0(self): 191 | 192 | # Define read_gpio0() returning 42 193 | @ffi.def_extern() 194 | def read_gpio0(): 195 | return 42 196 | 197 | self.assertEqual(module.read_gpio(0), 42) 198 | 199 | def test_read_gpio1(self): 200 | 201 | # Mock read_gpio1() to return 21 202 | read_gpio1 = unittest.mock.MagicMock(return_value=21) 203 | ffi.def_extern('read_gpio1')(read_gpio1) 204 | 205 | self.assertEqual(module.read_gpio(1), 21) 206 | 207 | # Check if mock was called once with no parameters 208 | read_gpio1.assert_called_once_with() 209 | 210 | if __name__ == '__main__': 211 | unittest.main() 212 | ``` 213 | 214 | ## Troubleshooting errors and problems 215 | 216 | If you have some trouble loading your C files into a module, take a look at the file [ERRORS.md](https://github.com/djboni/unit-test-c-with-python/blob/master/ERRORS.md) for known errors. 217 | 218 | ## References 219 | 220 | This is based on Alexander Steffen's presentations: 221 | 222 | * Alexander Steffen - [Writing unit tests for C code in Python](https://www.youtube.com/watch?v=zW_HyDTPjO0) - EuroPython Conference (21 July 2016) 223 | * Alexander Steffen - [Testing microcontroller firmware with Python](https://www.youtube.com/watch?v=-SvmjCWBX10) - EuroPython Conference (10 July 2017). 224 | 225 | Other useful presentations: 226 | 227 | * Benno Rice - [You Can't Unit Test C, Right?](https://www.youtube.com/watch?v=z-uWt5wVVkU) (How to test C with C) 228 | 229 | ### Frameworks for unit-tests 230 | 231 | * [cmocka](https://cmocka.org/) 232 | * [Check](https://libcheck.github.io/check/) 233 | * [ATF](https://github.com/jmmv/atf) and [Kyua](https://github.com/jmmv/kyua/) 234 | * [Acutest](https://github.com/mity/acutest) 235 | * [C++ Boost.Test](https://www.boost.org/doc/libs/1_75_0/libs/test/doc/html/index.html) 236 | * [Headlock - Python/C Bridge for Unittesting](https://pypi.org/project/headlock/) 237 | * [Python unittest](https://docs.python.org/3/library/unittest.html) 238 | 239 | ### Code coverage tools 240 | 241 | * [Gcov](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html) 242 | * [gcovr](https://gcovr.com/en/stable/) 243 | * [lcov](https://github.com/linux-test-project/lcov) and genhtml 244 | 245 | ## TODO 246 | 247 | * Is there a way to use Gcov to test statement or branch coverage when testing C code with Python? 248 | * Is there a way to access macros from the files? 249 | -------------------------------------------------------------------------------- /load.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Unit-Test C with Python: 4 | # https://github.com/djboni/unit-test-c-with-python 5 | # 6 | # Stuff you need to install: 7 | # sudo apt install gcc python3 python3-cffi python3-pycparser 8 | 9 | import subprocess 10 | import uuid 11 | import re 12 | import importlib 13 | import unittest.mock 14 | from typing import List, Optional, Union 15 | 16 | import cffi 17 | import pycparser.c_generator 18 | 19 | Remove_Unknowns = """\ 20 | #define __attribute__(x) 21 | #define __restrict 22 | """ 23 | 24 | 25 | def load( 26 | source_files: Union[List[str], str], 27 | include_paths: Optional[List[str]] = None, 28 | compiler_options: Optional[List[str]] = None, 29 | remove_unknowns: str = "", 30 | module_name: str = "pysim_", 31 | avoid_cache: bool = False, 32 | en_code_coverage: bool = False, 33 | en_sanitize_undefined: bool = False, 34 | en_sanitize_address: bool = False, 35 | ): # pylint: disable=too-many-arguments,too-many-locals,too-many-statements 36 | """ 37 | Load a C file into Python as a module. 38 | 39 | source_files: ['file1.c', file2.c'] or just 'file1.c' 40 | include_paths: ['.', './includes'] 41 | compiler_options: ['-std=c90', '-O0', '-Wall', '-Wextra'] 42 | 43 | module_name: sets the module name (and names of created files). 44 | 45 | avoid_cache=True: makes random names to allow testing code with global variables. 46 | 47 | en_code_coverage=True: enables Gcov code coverage. 48 | 49 | en_sanitize_undefined=True: enables undefined behavior sanitizer. 50 | 51 | en_sanitize_address=True: enables address sanitizer (not working). 52 | """ 53 | 54 | # Avoid caching using random name to module 55 | if avoid_cache: 56 | module_name += uuid.uuid4().hex + "_" 57 | 58 | # Create a list if just one souce file in a string 59 | if isinstance(source_files, str): 60 | source_files = [source_files] 61 | 62 | if include_paths is None: 63 | include_paths = [] 64 | 65 | if compiler_options is None: 66 | compiler_options = [] 67 | 68 | # Prepend -I on include paths 69 | include_paths = ["-I" + x for x in include_paths] 70 | 71 | # Collect source code 72 | source_content_list: List[str] = [] 73 | for file in source_files: 74 | with open(file, encoding="utf8") as fp: 75 | source_content_list.append(fp.read()) 76 | source_content: str = "\n".join(source_content_list) 77 | 78 | # Collect include files 79 | # TODO Remove inclusions inside comments 80 | header_content = "".join( 81 | x[0] 82 | for x in re.findall( 83 | r"(\s*\#\s*(" 84 | r"include\s|define\s|undef\s|if(n?def)?\s|else|endif|error|warning" 85 | r")[^\n\r\\]*((\\\n|\\\r|\\\n\r|\\\r\n)[^\n\r\\]*)*)", 86 | source_content, 87 | ) 88 | ) 89 | 90 | # Preprocess include files 91 | header_content = _RemoveStandardIncludes(header_content) 92 | header_content = Remove_Unknowns + remove_unknowns + header_content 93 | header_content = preprocess(header_content, include_paths, compiler_options) 94 | 95 | # Preprocess source code 96 | source_content = _RemoveStandardIncludes(source_content) 97 | source_content = Remove_Unknowns + remove_unknowns + source_content 98 | source_content = preprocess(source_content, include_paths, compiler_options) 99 | 100 | # Remove conflicts 101 | # header_content = header_content.replace('typedef struct { int __val[2]; } __fsid_t;', '') 102 | source_content = source_content.replace( 103 | "typedef struct { int __val[2]; } __fsid_t;", "" 104 | ) 105 | 106 | # Rename main, to avoid any conflicts, and declare it in the header 107 | if "int main(" in source_content: 108 | # TODO REGEX 109 | source_content = source_content.replace("int main(", "int mpmain(") 110 | header_content += "\nint mpmain(int argc, char **argv);\n" 111 | elif "void main(" in source_content: 112 | # TODO REGEX 113 | source_content = source_content.replace("void main(", "void mpmain(") 114 | header_content += "\nvoid mpmain(void argc, char **argv);\n" 115 | 116 | # TODO Compile only if source_content is different than module_name.c 117 | 118 | # Prepend 'extern "Python+C" ' to functions declarations with no definitions 119 | try: 120 | ast_header = pycparser.CParser().parse(header_content) 121 | header_generator = HeaderGenerator() 122 | header_generator.set_SourceContent(source_content) 123 | header_content = header_generator.visit(ast_header) 124 | except: 125 | print() 126 | print(80 * "-") 127 | print("HEADER:") 128 | print(header_content) 129 | print(80 * "-") 130 | print() 131 | raise 132 | 133 | # Run CFFI 134 | ffibuilder = cffi.FFI() 135 | ffibuilder.cdef(header_content) 136 | include_dirs = [x.replace("-I", "") for x in include_paths] 137 | 138 | extra_compile_args = [] 139 | extra_link_args = [] 140 | libraries = [] 141 | 142 | if en_sanitize_address: 143 | # Address sanitizer 144 | # export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.4 145 | extra_compile_args += ["-fsanitize=address"] 146 | # extra_link_args += ['-fsanitize=address', '-static-libasan'] 147 | # extra_link_args += ['-fsanitize=address', '-shared-libasan'] 148 | libraries += ["asan"] 149 | 150 | if en_sanitize_undefined: 151 | extra_compile_args += [ 152 | "-fsanitize=undefined", 153 | ] 154 | extra_link_args += [ 155 | "-fsanitize=undefined", 156 | ] 157 | 158 | if en_code_coverage: 159 | # Code coverage 160 | extra_compile_args += [ 161 | "--coverage", 162 | ] 163 | extra_link_args += [ 164 | "--coverage", 165 | ] 166 | libraries += [] 167 | 168 | ffibuilder.set_source( 169 | module_name, 170 | source_content, 171 | include_dirs=include_dirs, 172 | extra_compile_args=extra_compile_args, 173 | libraries=libraries, 174 | extra_link_args=extra_link_args, 175 | ) 176 | ffibuilder.compile() 177 | 178 | # Import and return resulting module 179 | module = importlib.import_module(module_name) 180 | 181 | # Return both the library object and the ffi object 182 | return module.lib, module.ffi 183 | 184 | 185 | def preprocess(source, include_paths, compiler_options): 186 | try: 187 | # command = ['gcc', *compiler_options, *include_paths, '-E', '-P', '-'] 188 | command = ( 189 | [ 190 | "gcc", 191 | ] 192 | + compiler_options 193 | + include_paths 194 | + ["-E", "-P", "-"] 195 | ) 196 | return subprocess.check_output( 197 | command, input=source, universal_newlines=True 198 | ) 199 | except: 200 | print() 201 | print(80 * "-") 202 | print("SOURCE/HEADER:") 203 | print(source) 204 | print(80 * "-") 205 | print() 206 | raise 207 | 208 | 209 | class HeaderGenerator(pycparser.c_generator.CGenerator): 210 | def __init__(self, *args, **kwargs): 211 | super().__init__(*args, **kwargs) 212 | self.functions = set() 213 | self.source_content = "" 214 | 215 | def set_SourceContent(self, source_content): 216 | self.source_content = source_content 217 | 218 | def visit_Decl(self, n, *args, **kwargs): 219 | result = super().visit_Decl(n, *args, **kwargs) 220 | 221 | if isinstance(n.type, pycparser.c_ast.FuncDecl): 222 | # Is a function declaration 223 | if n.name in self.functions: 224 | # Is already in functions 225 | pass 226 | elif ( 227 | re.search( 228 | ( 229 | re.escape(result) 230 | .replace("\\(", "\\(\\s*") 231 | .replace("\\)", "\\s*\\)") 232 | .replace("\\*", "\\*\\s*") 233 | .replace("\\ ", "\\s*") 234 | + "\\s*\\{" 235 | ), 236 | self.source_content, 237 | ) 238 | is not None 239 | ): 240 | # Is declared in source content 241 | pass 242 | else: 243 | # Not in functions, not in source 244 | self.functions.add(n.name) 245 | result = 'extern "Python+C" ' + result 246 | else: 247 | # Not a function declaration 248 | pass 249 | 250 | return result 251 | 252 | def visit_FuncDef(self, n): 253 | self.functions.add(n.decl.name) 254 | return "" 255 | 256 | 257 | class FFIMocks: 258 | """ 259 | Define 'extern "Pyton+C"' function as a mock object. 260 | 261 | from unit_test_c_with_python import load, FFIMocks 262 | module, ffi = load('gpio.c') 263 | 264 | Mocks = FFIMocks() 265 | 266 | # Create mocks 267 | Mocks.CreateMock(ffi, 'read_gpio0', return_value=42) 268 | Mocks.CreateMock(ffi, 'read_gpio1', return_value=21) 269 | 270 | # Reset mocks [Useful in setUp()] 271 | Mocks.ResetMocks() 272 | """ 273 | 274 | def CreateMock(self, ffi, name, *args, **kwargs): 275 | mock = unittest.mock.Mock(*args, **kwargs) 276 | setattr(self, name, mock) 277 | ffi.def_extern(name)(mock) 278 | 279 | def ResetMocks(self): 280 | for name in dir(self): 281 | obj = getattr(self, name) 282 | if isinstance(obj, unittest.mock.Mock): 283 | obj.reset_mock() 284 | 285 | 286 | def _RemoveStandardIncludes(source): 287 | return re.sub(r"#\s*include\s*<.*?>", "", source) 288 | --------------------------------------------------------------------------------