Skip to content

C UNIT TESTING!!! (this time on the right repo) #3477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9899cb4
Beginnings of c unit testing with base module (with necessary changes…
oddbookworm Jun 6, 2025
67e4043
Updated actions for ctest
oddbookworm Jun 6, 2025
8aa55bb
Unity now a wrap
oddbookworm Jun 6, 2025
8b7b141
Deleted failed debian build
oddbookworm Jun 6, 2025
66d8ca3
Messed with ctest build to try to get it to work on CI... failed
oddbookworm Jun 7, 2025
4339eaa
Merge branch 'main' of https://github.com/pygame-community/pygame-ce …
oddbookworm Jun 7, 2025
d807912
CI updates
oddbookworm Jun 7, 2025
fae7203
.gitignore subprojects directory
oddbookworm Jun 7, 2025
89988c9
ctest is now python package
oddbookworm Jun 7, 2025
5d658fd
Fixed merge conflict with #3483
oddbookworm Jun 7, 2025
66801ec
Finished proper ctest support in ubuntu-checks action
oddbookworm Jun 7, 2025
5b9e484
Fixed ctest log upload step
oddbookworm Jun 7, 2025
cdf2baf
Formatting
oddbookworm Jun 7, 2025
43104f8
Workflow no longer tries to upload nonexistent artifact and intention…
oddbookworm Jun 7, 2025
4423428
Fixed merge conflict with #3483
oddbookworm Jun 7, 2025
a9b0e98
Properly reference matrix vars
oddbookworm Jun 7, 2025
99920aa
Maybe fixed cppcheck diff
oddbookworm Jun 7, 2025
4587071
Merge commit 'refs/pull/3483/head' of https://github.com/pygame-commu…
oddbookworm Jun 7, 2025
b8184e4
Split off declarations of base module to header and exposed everythin…
oddbookworm Jun 7, 2025
e7bfb29
Split off base declarations to base.h, and remove static linkage for …
oddbookworm Jun 8, 2025
2aa120f
Should fix WASM build
oddbookworm Jun 8, 2025
f9298f6
Fix cppcheck skip logic on CI
ankith26 Jun 8, 2025
aa3b89e
Merge branch 'main' of https://github.com/pygame-community/pygame-ce …
oddbookworm Jun 9, 2025
9e9f13d
Merge commit 'refs/pull/3489/head' of https://github.com/pygame-commu…
oddbookworm Jun 9, 2025
6b34823
Fixed merge conflict with #3486
oddbookworm Jun 9, 2025
9f4fd1c
Code cleanup, reverted unnecessary changes
oddbookworm Jun 9, 2025
0af9f90
Code formatting
oddbookworm Jun 9, 2025
b850dad
Make slightly less clever use of token pasting to make gcc happy
oddbookworm Jun 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/run-ubuntu-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:
id: build-pygame-ce
run: |
pyenv global ${{ matrix.python }}-debug
python dev.py build --lax --coverage
python dev.py build --lax --coverage --ctest

- name: Run tests
env:
Expand Down Expand Up @@ -135,13 +135,19 @@ jobs:

steps:
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # fetch full history

- name: Check if any src_c files changed
id: check-changes
continue-on-error: true
run: |
git fetch origin ${{ github.base_ref }} --depth=1 || true
git checkout ${{ github.base_ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
else
CHANGED_FILES=$(git diff --name-only HEAD^1...HEAD)
fi
echo "Changed files: $CHANGED_FILES"
echo "$CHANGED_FILES" | grep '^src_c/' || echo "skip=true" >> "$GITHUB_OUTPUT"

- name: Install cppcheck
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
# Ruff
.ruff_cache

# Meson subprojects
subprojects/*
!subprojects/*.wrap

# Other
envdev*
.virtualenv*
Expand Down
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ repos:
| ^.*\.svg$
| ^.*\.sfd$
| docs/LGPL.txt
| subprojects/.*
)$
- id: trailing-whitespace
exclude: |
Expand All @@ -23,6 +24,7 @@ repos:
| ^.*\.svg$
| ^.*\.sfd$
| docs/LGPL.txt
| subprojects/.*
)$

- repo: https://github.com/astral-sh/ruff-pre-commit
Expand All @@ -47,4 +49,5 @@ repos:
| src_c/include/sse2neon.h
| src_c/include/pythoncapi_compat.h
| src_c/pypm.c
| subprojects/.*
)$
121 changes: 121 additions & 0 deletions ctest/base_ctest.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#include <Python.h>

#include "base.h"
#include "test_common.h"

static PyObject *base_module;

/* setUp and tearDown must be nonstatic void(void) */
void setUp(void) {}

void tearDown(void) {}

/**
* @brief Tests _pg_is_int_tuple when passed a tuple of ints
*/
PG_CTEST(test__pg_is_int_tuple_nominal)(PyObject *self, PyObject *_null) {
PyObject *arg1 = Py_BuildValue("(iii)", 1, 2, 3);
PyObject *arg2 = Py_BuildValue("(iii)", -1, -2, -3);
PyObject *arg3 = Py_BuildValue("(iii)", 1, -2, -3);

TEST_ASSERT_EQUAL(1, _pg_is_int_tuple(arg1));
TEST_ASSERT_EQUAL(1, _pg_is_int_tuple(arg2));
TEST_ASSERT_EQUAL(1, _pg_is_int_tuple(arg3));

Py_RETURN_NONE;
}

/**
* @brief Tests _pg_is_int_tuple when passed a tuple of non-numeric values
*/
PG_CTEST(test__pg_is_int_tuple_failureModes)(PyObject *self, PyObject *_null) {
PyObject *arg1 =
Py_BuildValue("(sss)", (char *)"Larry", (char *)"Moe", (char *)"Curly");
PyObject *arg2 = Py_BuildValue("(sss)", (char *)NULL, (char *)NULL,
(char *)NULL); // tuple of None's
PyObject *arg3 = Py_BuildValue("(OOO)", arg1, arg2, arg1);

TEST_ASSERT_EQUAL(0, _pg_is_int_tuple(arg1));
TEST_ASSERT_EQUAL(0, _pg_is_int_tuple(arg2));
TEST_ASSERT_EQUAL(0, _pg_is_int_tuple(arg3));

Py_RETURN_NONE;
}

/**
* @brief Tests _pg_is_int_tuple when passed a tuple of floats
*/
PG_CTEST(test__pg_is_int_tuple_floats)(PyObject *self, PyObject *_null) {
PyObject *arg1 = Py_BuildValue("(ddd)", 1.0, 2.0, 3.0);
PyObject *arg2 = Py_BuildValue("(ddd)", -1.1, -2.2, -3.3);
PyObject *arg3 = Py_BuildValue("(ddd)", 1.0, -2.0, -3.1);

TEST_ASSERT_EQUAL(0, _pg_is_int_tuple(arg1));
TEST_ASSERT_EQUAL(0, _pg_is_int_tuple(arg2));
TEST_ASSERT_EQUAL(0, _pg_is_int_tuple(arg3));

Py_RETURN_NONE;
}

/*=======Test Reset Option=====*/
/* This must be void(void) */
void resetTest(void) {
tearDown();
setUp();
}

/*=======Exposed Test Reset Option=====*/
static PyObject *reset_test(PyObject *self, PyObject *_null) {
resetTest();

Py_RETURN_NONE;
}

/*=======Run The Tests=======*/
static PyObject *run_tests(PyObject *self, PyObject *_null) {
UnityBegin("base_ctest.c");
RUN_TEST_PG_INTERNAL(test__pg_is_int_tuple_nominal);
RUN_TEST_PG_INTERNAL(test__pg_is_int_tuple_failureModes);
RUN_TEST_PG_INTERNAL(test__pg_is_int_tuple_floats);

return PyLong_FromLong(UnityEnd());
}

static PyMethodDef base_test_methods[] = {
{"test__pg_is_int_tuple_nominal",
(PyCFunction)test__pg_is_int_tuple_nominal, METH_NOARGS,
"Tests _pg_is_int_tuple when passed a tuple of ints"},
{"test__pg_is_int_tuple_failureModes",
(PyCFunction)test__pg_is_int_tuple_failureModes, METH_NOARGS,
"Tests _pg_is_int_tuple when passed a tuple of non-numeric values"},
{"test__pg_is_int_tuple_floats", (PyCFunction)test__pg_is_int_tuple_floats,
METH_NOARGS, "Tests _pg_is_int_tuple when passed a tuple of floats"},
{"reset_test", (PyCFunction)reset_test, METH_NOARGS,
"Resets the test suite between tests, run_tests automatically calls this "
"after each test case it calls"},
{"run_tests", (PyCFunction)run_tests, METH_NOARGS,
"Runs all the tests in this test wuite"},
{NULL, NULL, 0, NULL}};

MODINIT_DEFINE(base_ctest) {
PyObject *module;

static struct PyModuleDef _module = {
PyModuleDef_HEAD_INIT,
"base_ctest",
"C unit tests for the pygame.base internal implementation",
-1,
base_test_methods,
NULL,
NULL,
NULL,
NULL};

/* create the module */
module = PyModule_Create(&_module);
if (!module) {
return NULL;
}

return module;
}
13 changes: 13 additions & 0 deletions ctest/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
unity_subproject = subproject('unity')
unity_dependency = unity_subproject.get_variable('unity_dep')

base_ctest = py.extension_module(
'base_ctest',
'base_ctest.c',
c_args: warnings_error,
dependencies: [pg_base_deps, unity_dependency],
sources: ['../src_c/base.c'],
install: true,
subdir: pg,
include_directories: ['../src_c']
)
48 changes: 48 additions & 0 deletions ctest/test_common.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <Python.h>

#include "unity.h"

#ifndef TEST_COMMON_H
#define TEST_COMMON_H

struct TestCase {
char *test_name;
int line_num;
};

/*
This will take some explanation... the PG_CTEST macro defines two things
for an individual test case. The test case itself, and a struct instance
called meta_TEST_CASE_NAME. The struct has two pieces of important
information that unity needs: the name in string format and the line
number of the test. This would be an absolute nighmare to maintain by
hand, so I defined a macro to do it automagically for us.

The RUN_TEST_PG_INTERNAL macro then references that struct for each test
case that we tell it about and automatically populates the unity fields
with the requisite data.

Note that the arguments to the test function must be *exactly*
(PyObject * self, PyObject * _null), but due to gcc throwing a fit, I
cannot just use token pasting to have the macro generate that part for me
*/
#define PG_CTEST(TestFunc) \
static struct TestCase meta_##TestFunc = {#TestFunc, __LINE__}; \
static PyObject *TestFunc

#define RUN_TEST_PG_INTERNAL(TestFunc) \
{ \
Unity.CurrentTestName = meta_##TestFunc.test_name; \
Unity.CurrentTestLineNumber = meta_##TestFunc.line_num; \
Unity.NumberOfTests++; \
if (TEST_PROTECT()) { \
setUp(); \
TestFunc(self, _null); \
} \
if (TEST_PROTECT()) { \
tearDown(); \
} \
UnityConcludeTest(); \
}

#endif // #ifndef TEST_COMMON_H
11 changes: 11 additions & 0 deletions dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
]
COVERAGE_ARGS = ["-Csetup-args=-Dcoverage=true"]

CTEST_ARGS = ["-Csetup-args=-Dctest=true"]

# We assume this script works with any pip version above this.
PIP_MIN_VERSION = "23.1"

Expand Down Expand Up @@ -208,6 +210,7 @@ def cmd_build(self):
lax = self.args.get("lax", False)
sdl3 = self.args.get("sdl3", False)
coverage = self.args.get("coverage", False)
ctest = self.args.get("ctest", False)
if wheel_dir and coverage:
pprint("Cannot pass --wheel and --coverage together", Colors.RED)
sys.exit(1)
Expand All @@ -221,6 +224,8 @@ def cmd_build(self):
build_suffix += "-sdl3"
if coverage:
build_suffix += "-cov"
if ctest:
build_suffix += "-ctest"
install_args = [
"--no-build-isolation",
f"-Cbuild-dir=.mesonpy-build{build_suffix}",
Expand All @@ -245,6 +250,9 @@ def cmd_build(self):
if coverage:
install_args.extend(COVERAGE_ARGS)

if ctest:
install_args.extend(CTEST_ARGS)

info_str = f"with {debug=}, {lax=}, {sdl3=}, and {coverage=}"
if wheel_dir:
pprint(f"Building wheel at '{wheel_dir}' ({info_str})")
Expand Down Expand Up @@ -376,6 +384,9 @@ def parse_args(self):
"supported if the underlying compiler supports the --coverage argument"
),
)
build_parser.add_argument(
"--ctest", action="store_true", help="Build the C-direct unit tests"
)

# Docs command
docs_parser = subparsers.add_parser("docs", help="Generate docs")
Expand Down
5 changes: 5 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,9 @@ if not get_option('stripped')
subdir('buildconfig/stubs')
install_subdir('examples', install_dir: pg_dir, install_tag: 'pg-tag')
# TODO: install headers? not really important though

if get_option('ctest')
subproject('unity')
subdir('ctest')
endif
endif
10 changes: 7 additions & 3 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ option('midi', type: 'feature', value: 'enabled')
# Controls whether to make a "stripped" pygame install. Enabling this disables
# the bundling of docs/examples/tests/stubs in the wheels.
# The default behaviour is to bundle all of these.
option('stripped', type: 'boolean', value: 'false')
option('stripped', type: 'boolean', value: false)

# Controls whether to compile with -Werror (or its msvc equivalent). The default
# behaviour is to not do this by default
option('error_on_warns', type: 'boolean', value: 'false')
option('error_on_warns', type: 'boolean', value: false)

# Controls whether to error on build if generated docs are missing. Defaults to
# false.
option('error_docs_missing', type: 'boolean', value: 'false')
option('error_docs_missing', type: 'boolean', value: false)

# Controls whether to do a coverage build.
# This argument must be used together with the editable install.
option('coverage', type: 'boolean', value: false)

# Controls whether to do to a C unit test build. Defaults to false.
# If "stripped" is true, this is ignored.
option('ctest', type: 'boolean', value: false)

# Controls whether to use SDL3 instead of SDL2. The default is to use SDL2
option('sdl_api', type: 'integer', min: 2, max: 3, value: 2)
Loading
Loading