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 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
5 changes: 3 additions & 2 deletions .github/workflows/run-ubuntu-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Also generates coverage information from unit tests. Note that for intrinsics,
# it only runs what gets compiled and would naturally run. It also is limited to
# what can run in a CI environment.

# Update this workflow when our min/max python minor versions update
# This workflow is necessary to ensure that we can build and run with
# a debug python build without too much worrying about SIGABRT being thrown
Expand All @@ -28,6 +29,7 @@ on:
# re-include current file to not be excluded
- '!.github/workflows/run-ubuntu-checks.yml'


pull_request:
branches: main
paths-ignore:
Expand Down Expand Up @@ -101,8 +103,7 @@ jobs:
id: build-pygame-ce
run: |
pyenv global ${{ matrix.python }}-debug
python dev.py build --lax --coverage --sanitize undefined

python dev.py build --lax --coverage --ctest --sanitize undefined
- name: Run tests
env:
SDL_VIDEODRIVER: "dummy"
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
16 changes: 13 additions & 3 deletions dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 @@ -213,6 +215,7 @@ def cmd_build(self):
stripped = self.args.get("stripped", False)
sanitize = self.args.get("sanitize")
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 @@ -226,6 +229,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 Down Expand Up @@ -255,12 +260,14 @@ def cmd_build(self):
if coverage:
install_args.extend(COVERAGE_ARGS)

if ctest:
install_args.extend(CTEST_ARGS)

if sanitize:
install_args.append(f"-Csetup-args=-Db_sanitize={sanitize}")

info_str = (
f"with {debug=}, {lax=}, {sdl3=}, {stripped=}, {coverage=} and {sanitize=}"
)
info_str = f"with {debug=}, {lax=}, {sdl3=}, {stripped=}, {coverage=}, {ctest=}, and {sanitize=}"

if wheel_dir:
pprint(f"Building wheel at '{wheel_dir}' ({info_str})")
cmd_run(
Expand Down Expand Up @@ -416,6 +423,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 @@ -452,4 +452,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)
3 changes: 2 additions & 1 deletion src_c/base.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/*
pygame-ce - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
Expand Down Expand Up @@ -45,7 +46,7 @@ static int pg_is_init = 0;
static bool pg_sdl_was_init = 0;
SDL_Window *pg_default_window = NULL;
pgSurfaceObject *pg_default_screen = NULL;
static int pg_env_blend_alpha_SDL2 = 0;
int pg_env_blend_alpha_SDL2 = 0;

/* compare compiled to linked, raise python error on incompatibility */
int
Expand Down
3 changes: 3 additions & 0 deletions subprojects/unity.wrap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[wrap-git]
url = https://github.com/ThrowTheSwitch/Unity.git
revision = v2.6.1
17 changes: 17 additions & 0 deletions test/ctest_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import unittest

base_ctest = None
try:
import pygame.base_ctest as base_ctest
except ModuleNotFoundError:
pass


class Ctest(unittest.TestCase):
@unittest.skipIf(base_ctest is None, "base_ctest not built")
def test_run_base_ctests(self):
self.assertEqual(base_ctest.run_tests(), 0)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ test_files = files(
'color_test.py',
'constants_test.py',
'controller_test.py',
'ctest_test.py',
'cursors_test.py',
'debug_test.py',
'display_test.py',
Expand Down
Loading