| #import <XCTest/XCTest.h> |
| #import <Python/Python.h> |
| |
| @interface TestbedTests : XCTestCase |
| |
| @end |
| |
| @implementation TestbedTests |
| |
| |
| - (void)testPython { |
| const char **argv; |
| int exit_code; |
| int failed; |
| PyStatus status; |
| PyPreConfig preconfig; |
| PyConfig config; |
| PyObject *app_packages_path; |
| PyObject *method_args; |
| PyObject *result; |
| PyObject *site_module; |
| PyObject *site_addsitedir_attr; |
| PyObject *sys_module; |
| PyObject *sys_path_attr; |
| NSArray *test_args; |
| NSString *python_home; |
| NSString *path; |
| wchar_t *wtmp_str; |
| |
| NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; |
| |
| // Set some other common environment indicators to disable color, as the |
| // Xcode log can't display color. Stdout will report that it is *not* a |
| // TTY. |
| setenv("NO_COLOR", "1", true); |
| setenv("PYTHON_COLORS", "0", true); |
| |
| if (getenv("GITHUB_ACTIONS")) { |
| NSLog(@"Running in a GitHub Actions environment"); |
| } |
| // Arguments to pass into the test suite runner. |
| // argv[0] must identify the process; any subsequent arg |
| // will be handled as if it were an argument to `python -m test` |
| // The processInfo arguments contain the binary that is running, |
| // followed by the arguments defined in the test plan. This means: |
| // run_module = test_args[1] |
| // argv = ["Testbed"] + test_args[2:] |
| test_args = [[NSProcessInfo processInfo] arguments]; |
| if (test_args == NULL) { |
| NSLog(@"Unable to identify test arguments."); |
| } |
| NSLog(@"Test arguments: %@", test_args); |
| argv = malloc(sizeof(char *) * ([test_args count] - 1)); |
| argv[0] = "Testbed"; |
| for (int i = 1; i < [test_args count] - 1; i++) { |
| argv[i] = [[test_args objectAtIndex:i+1] UTF8String]; |
| } |
| |
| // Generate an isolated Python configuration. |
| NSLog(@"Configuring isolated Python..."); |
| PyPreConfig_InitIsolatedConfig(&preconfig); |
| PyConfig_InitIsolatedConfig(&config); |
| |
| // Configure the Python interpreter: |
| // Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale. |
| // See https://docs.python.org/3/library/os.html#python-utf-8-mode. |
| preconfig.utf8_mode = 1; |
| // Use the system logger for stdout/err |
| config.use_system_logger = 1; |
| // Don't buffer stdio. We want output to appears in the log immediately |
| config.buffered_stdio = 0; |
| // Don't write bytecode; we can't modify the app bundle |
| // after it has been signed. |
| config.write_bytecode = 0; |
| // Ensure that signal handlers are installed |
| config.install_signal_handlers = 1; |
| // Run the test module. |
| config.run_module = Py_DecodeLocale([[test_args objectAtIndex:1] UTF8String], NULL); |
| // For debugging - enable verbose mode. |
| // config.verbose = 1; |
| |
| NSLog(@"Pre-initializing Python runtime..."); |
| status = Py_PreInitialize(&preconfig); |
| if (PyStatus_Exception(status)) { |
| XCTFail(@"Unable to pre-initialize Python interpreter: %s", status.err_msg); |
| PyConfig_Clear(&config); |
| return; |
| } |
| |
| // Set the home for the Python interpreter |
| python_home = [NSString stringWithFormat:@"%@/python", resourcePath, nil]; |
| NSLog(@"PythonHome: %@", python_home); |
| wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL); |
| status = PyConfig_SetString(&config, &config.home, wtmp_str); |
| if (PyStatus_Exception(status)) { |
| XCTFail(@"Unable to set PYTHONHOME: %s", status.err_msg); |
| PyConfig_Clear(&config); |
| return; |
| } |
| PyMem_RawFree(wtmp_str); |
| |
| // Read the site config |
| status = PyConfig_Read(&config); |
| if (PyStatus_Exception(status)) { |
| XCTFail(@"Unable to read site config: %s", status.err_msg); |
| PyConfig_Clear(&config); |
| return; |
| } |
| |
| NSLog(@"Configure argc/argv..."); |
| status = PyConfig_SetBytesArgv(&config, [test_args count] - 1, (char**) argv); |
| if (PyStatus_Exception(status)) { |
| XCTFail(@"Unable to configure argc/argv: %s", status.err_msg); |
| PyConfig_Clear(&config); |
| return; |
| } |
| |
| NSLog(@"Initializing Python runtime..."); |
| status = Py_InitializeFromConfig(&config); |
| if (PyStatus_Exception(status)) { |
| XCTFail(@"Unable to initialize Python interpreter: %s", status.err_msg); |
| PyConfig_Clear(&config); |
| return; |
| } |
| |
| // Add app_packages as a site directory. This both adds to sys.path, |
| // and ensures that any .pth files in that directory will be executed. |
| site_module = PyImport_ImportModule("site"); |
| if (site_module == NULL) { |
| XCTFail(@"Could not import site module"); |
| return; |
| } |
| |
| site_addsitedir_attr = PyObject_GetAttrString(site_module, "addsitedir"); |
| if (site_addsitedir_attr == NULL || !PyCallable_Check(site_addsitedir_attr)) { |
| XCTFail(@"Could not access site.addsitedir"); |
| return; |
| } |
| |
| path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil]; |
| NSLog(@"App packages path: %@", path); |
| wtmp_str = Py_DecodeLocale([path UTF8String], NULL); |
| app_packages_path = PyUnicode_FromWideChar(wtmp_str, wcslen(wtmp_str)); |
| if (app_packages_path == NULL) { |
| XCTFail(@"Could not convert app_packages path to unicode"); |
| return; |
| } |
| PyMem_RawFree(wtmp_str); |
| |
| method_args = Py_BuildValue("(O)", app_packages_path); |
| if (method_args == NULL) { |
| XCTFail(@"Could not create arguments for site.addsitedir"); |
| return; |
| } |
| |
| result = PyObject_CallObject(site_addsitedir_attr, method_args); |
| if (result == NULL) { |
| XCTFail(@"Could not add app_packages directory using site.addsitedir"); |
| return; |
| } |
| |
| // Add test code to sys.path |
| sys_module = PyImport_ImportModule("sys"); |
| if (sys_module == NULL) { |
| XCTFail(@"Could not import sys module"); |
| return; |
| } |
| |
| sys_path_attr = PyObject_GetAttrString(sys_module, "path"); |
| if (sys_path_attr == NULL) { |
| XCTFail(@"Could not access sys.path"); |
| return; |
| } |
| |
| path = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; |
| NSLog(@"App path: %@", path); |
| wtmp_str = Py_DecodeLocale([path UTF8String], NULL); |
| failed = PyList_Insert(sys_path_attr, 0, PyUnicode_FromString([path UTF8String])); |
| if (failed) { |
| XCTFail(@"Unable to add app to sys.path"); |
| return; |
| } |
| PyMem_RawFree(wtmp_str); |
| |
| // Ensure the working directory is the app folder. |
| chdir([path UTF8String]); |
| |
| // Start the test suite. Print a separator to differentiate Python startup logs from app logs |
| NSLog(@"---------------------------------------------------------------------------"); |
| |
| exit_code = Py_RunMain(); |
| XCTAssertEqual(exit_code, 0, @"Test suite did not pass"); |
| |
| NSLog(@"---------------------------------------------------------------------------"); |
| |
| Py_Finalize(); |
| } |
| |
| |
| @end |