From 9638c331b0120a6267af785b005053687c73003f Mon Sep 17 00:00:00 2001 From: said Date: Mon, 10 Aug 2026 16:01:29 +0100 Subject: [PATCH 1/3] reorganize tests folders --- .github/workflows/coverage.yml | 5 +- .github/workflows/docs.yml | 2 +- .github/workflows/merge-validation.yml | 17 +- .github/workflows/tests.yml | 10 +- AGENTS.md | 2 +- CHANGELOG.md | 7 +- README.md | 53 +- docs/developer/development-workflow.md | 20 +- docs/developer/feature-to-code-map.md | 6 +- docs/developer/repository-structure.md | 11 +- docs/developer/source-map.md | 14 +- docs/developer/testing-strategy.md | 59 +- .../documentation-content-checklist.md | 2 +- .../fortran-test-suite-cleanup-checklist.md | 54 +- .../roadmap/semantic-pyi-wrapper-checklist.md | 4 +- .../wrapper-plan-migration-checklist.md | 2 +- docs/user/language-support/feature-matrix.md | 4 +- docs/user/reference/configuration-files.md | 8 +- prik/semantics/README.md | 18 +- tests/README.md | 50 +- tests/architecture/README.md | 23 - .../c/test_c_language_quarantine.py | 53 - .../fortran/test_ci_toolchain_lanes.py | 177 -- .../fortran/test_contract_coverage_map.py | 155 -- .../fortran/test_language_ownership.py | 215 --- .../fortran/test_smoke_selection.py | 140 -- tests/c/README.md | 3 - tests/c/_support/cli.py | 3 - tests/c/_support/parser_properties.py | 25 +- tests/c/_support/preprocessing.py | 35 +- tests/c/_support/semantic_properties.py | 18 +- tests/c/cli/test_c_cli_argument_contract.py | 4 +- tests/c/cli/test_c_cli_output_contract.py | 5 +- tests/c/cli/test_c_cli_stage_dispatch.py | 2 +- tests/c/parsing/test_c_parser_properties.py | 17 +- .../preprocessing/test_c_preprocessing_cli.py | 9 +- .../test_c_preprocessing_configuration.py | 14 +- .../test_c_preprocessing_dependencies.py | 12 +- .../test_c_preprocessing_execution.py | 16 +- .../test_c_preprocessing_properties.py | 9 +- tests/c/preprocessing/test_error_paths.py | 14 +- tests/c/preprocessing/test_source_mappings.py | 9 +- tests/c/semantics/conversion/_support.py | 153 -- .../test_c_conversion_properties.py | 12 +- .../test_functions_and_callbacks.py | 15 +- .../test_projects_and_diagnostics.py | 27 +- .../conversion/test_records_and_enums.py | 29 +- .../conversion/test_types_and_constants.py | 21 +- tests/docs/README.md | 15 + tests/docs/_structure_support.py | 629 +++++++ tests/{shared => }/docs/test_examples.py | 2 +- tests/docs/test_metadata_and_visibility.py | 68 + tests/docs/test_navigation.py | 381 ++++ tests/{shared => }/docs/test_publication.py | 0 tests/docs/test_reference_and_source_map.py | 232 +++ tests/docs/test_user_content.py | 457 +++++ tests/fortran/README.md | 16 +- tests/fortran/_support/fixture_conversion.py | 4 - .../fortran/_support/native_array_handles.py | 49 - tests/fortran/_support/ownership_policy.py | 112 -- tests/fortran/_support/parser_procedures.py | 31 +- tests/fortran/_support/parser_properties.py | 46 +- tests/fortran/_support/parser_regressions.py | 30 - tests/fortran/_support/printer_models.py | 62 - tests/fortran/_support/pyi_conversion.py | 115 +- tests/fortran/_support/semantic_conversion.py | 110 -- tests/fortran/_support/semantic_properties.py | 41 +- .../parsing/test_allocatable_declarations.py | 4 +- .../test_allocatable_contract_printing.py | 8 +- ...st_allocatable_module_contract_printing.py | 10 +- ...st_allocatable_output_contract_printing.py | 8 +- .../policy/test_allocatable_handle_policy.py | 20 +- .../test_allocatable_array_actual_abi.py | 10 +- .../test_allocatable_descriptor_abi.py | 10 +- .../test_allocatable_handle_protocol.py | 8 +- .../test_fortran_allocatable_semantics.py | 8 +- .../test_pyi_allocatable_semantics.py | 2 +- .../test_array_declarations_and_shapes.py | 6 +- .../arrays/policy/test_array_shape_policy.py | 8 +- .../arrays/semantics/test_array_semantics.py | 10 +- .../test_declaration_expression_utilities.py | 18 + .../test_callback_contract_printing.py | 4 +- .../test_fortran_callback_semantics.py | 14 +- .../semantics/test_pyi_callback_semantics.py | 8 +- .../pipeline/_support.py | 48 - .../pipeline/test_argument_contract.py | 18 +- .../pipeline/test_output_contract.py | 26 +- .../pipeline/test_stage_dispatch.py | 30 +- .../parsing/test_declarations_and_shapes.py | 8 +- .../test_scalar_kind_parsing_properties.py | 10 +- .../test_fortran_scalar_semantics.py | 12 +- .../semantics/test_types_and_storage.py | 16 +- .../parsing/test_derived_procedure_syntax.py | 4 +- .../parsing/test_derived_type_declarations.py | 4 +- .../parsing/test_derived_type_properties.py | 10 +- .../test_parameterized_derived_types.py | 6 +- .../parsing/test_type_bound_diagnostics.py | 10 +- .../policy/test_derived_accessor_policy.py | 20 +- .../policy/test_derived_policy_defaults.py | 14 +- .../semantics/test_derived_type_identity.py | 8 +- .../test_fortran_derived_semantics.py | 22 +- .../test_imported_derived_semantics.py | 22 +- .../semantics/test_pyi_class_semantics.py | 6 +- .../parsing/test_enum_diagnostics.py | 3 +- .../test_enum_compile_time_values.py | 4 +- .../semantics/test_pyi_enum_constants.py | 6 +- .../test_unsupported_enum_classes.py | 6 +- ...est_procedure_and_interface_regressions.py | 10 +- ...an_conversion_procedures_and_interfaces.py | 16 +- .../test_fortran_function_semantics.py | 8 +- .../parsing/test_generic_interface_syntax.py | 6 +- .../test_fortran_generic_semantics.py | 6 +- .../semantics/test_pyi_overload_semantics.py | 3 +- .../test_native_support.py} | 11 - .../infrastructure/codegen/test_binding.py | 39 + .../infrastructure/codegen/test_bridge.py | 18 + ...n_package_boundaries.py => test_checks.py} | 47 +- .../infrastructure/codegen/test_docstrings.py | 18 + .../infrastructure/codegen/test_generator.py | 532 ++++++ .../infrastructure/codegen/test_package.py | 67 + .../infrastructure/codegen/test_plan.py | 18 + ...per_plan_validation.py => test_planner.py} | 185 +- .../codegen/test_pyi_printer.py | 18 + .../test_scalar_boundary_validation.py | 105 -- ...foundations.py => test_source_printers.py} | 33 +- .../codegen/test_wrapper_assembly.py | 230 --- .../infrastructure}/naming/test_policy.py | 0 .../pipeline/test_wrapper_artifacts.py | 70 + .../semantics/test_native_array_handles.py | 45 + .../test_ownership.py} | 145 +- .../test_policy_completion.py} | 112 +- .../test_wrapper_policy.py | 12 + .../types/test_mapping_report.py | 2 +- .../infrastructure}/types/test_numpy.py | 0 .../utilities/test_class_visitor.py} | 0 .../infrastructure}/utilities/test_strings.py | 0 .../runtime/test_handle_lifecycle.py | 18 +- .../test_native_array_actual_handoff.py | 8 +- .../policy/test_module_variable_policy.py | 8 +- .../test_module_contract_semantics.py | 6 +- .../semantics/test_modules_and_imports.py | 10 +- .../test_optional_fortran_semantics.py | 8 +- .../semantics/test_optional_pyi_semantics.py | 10 +- .../policy/test_pointer_ownership_policy.py | 28 +- .../runtime/test_pointer_array_actual_abi.py | 10 +- .../runtime/test_pointer_descriptor_abi.py | 14 +- .../runtime/test_pointer_handle_protocol.py | 14 +- .../semantics/test_pointer_semantics.py | 4 +- .../test_export_and_initializer_policy.py | 8 +- .../semantics/test_module_initializers.py | 3 +- .../test_method_and_constructor_contracts.py | 12 +- .../semantics/test_raw_address_semantics.py | 10 +- .../semantics/test_compile_time_values.py | 28 +- .../test_fortran_conversion_properties.py | 14 +- .../test_semantic_conversion_smoke.py | 6 +- ...test_semantic_specialization_properties.py | 22 +- .../parsing/test_python_ast_contracts.py | 18 +- .../test_calls_and_policy_metadata.py | 16 +- .../pipeline/test_classes_and_methods.py | 20 +- .../test_pyi_printer_conversion_smoke.py | 6 +- .../test_pyi_printer_imports_and_packages.py | 22 +- .../pipeline/test_types_and_declarations.py | 16 +- .../semantics/test_calls_and_projections.py | 24 +- .../semantics/test_classes_and_overloads.py | 6 +- .../semantics/test_imports_and_packages.py | 28 +- .../semantics/test_round_trip_properties.py | 16 +- .../semantics/test_types_and_values.py | 26 +- .../test_declaration_and_scope_regressions.py | 12 +- .../test_derived_types_and_program_units.py | 10 +- ...ortran_parser_procedures_and_interfaces.py | 12 +- .../parsing/test_fortran_parser_properties.py | 18 +- ...source_form_and_diagnostics_regressions.py | 18 +- .../preprocessing/_support.py | 46 +- .../preprocessing/test_cli.py | 13 +- .../test_configuration_and_adapters.py | 14 +- .../test_dependencies_and_includes.py | 10 +- .../preprocessing/test_execution.py | 10 +- .../test_preprocessing_properties.py | 26 +- .../parsing/test_character_declarations.py | 4 +- .../test_fixed_form_character_parsing.py | 4 +- .../test_fortran_string_semantics.py | 8 +- .../semantics/test_string_pyi_semantics.py | 8 +- .../test_subroutine_argument_projection.py | 10 +- tests/shared/README.md | 23 - .../test_dependency_boundaries.py | 67 - .../architecture/test_package_structure.py | 11 - .../architecture/test_test_suite_layout.py | 616 ------ tests/shared/docs/test_structure.py | 1674 ----------------- tests/tools/README.md | 11 + .../{shared => }/tools/test_benchmark_host.py | 0 .../tools/test_build_time_benchmark.py | 0 .../tools/test_check_benchmark_regression.py | 0 .../tools/test_check_radon_policy.py | 0 .../test_check_static_analysis_versions.py | 0 .../tools/test_generate_performance_docs.py | 0 .../tools/test_print_pytest_failures.py | 0 .../tools/test_run_fortran_toolchain_lane.py | 0 .../tools/test_runtime_benchmark.py | 0 .../test_warm_real_library_native_cache.py | 0 tests/workflows/README.md | 13 + tests/workflows/test_release.py | 20 + 201 files changed, 3755 insertions(+), 5530 deletions(-) delete mode 100644 tests/architecture/README.md delete mode 100644 tests/architecture/c/test_c_language_quarantine.py delete mode 100644 tests/architecture/fortran/test_ci_toolchain_lanes.py delete mode 100644 tests/architecture/fortran/test_contract_coverage_map.py delete mode 100644 tests/architecture/fortran/test_language_ownership.py delete mode 100644 tests/architecture/fortran/test_smoke_selection.py create mode 100644 tests/docs/README.md create mode 100644 tests/docs/_structure_support.py rename tests/{shared => }/docs/test_examples.py (99%) create mode 100644 tests/docs/test_metadata_and_visibility.py create mode 100644 tests/docs/test_navigation.py rename tests/{shared => }/docs/test_publication.py (100%) create mode 100644 tests/docs/test_reference_and_source_map.py create mode 100644 tests/docs/test_user_content.py rename tests/fortran/infrastructure/{codegen/test_native_binding_support.py => binding_support/test_native_support.py} (86%) create mode 100644 tests/fortran/infrastructure/codegen/test_binding.py create mode 100644 tests/fortran/infrastructure/codegen/test_bridge.py rename tests/fortran/infrastructure/codegen/{test_codegen_package_boundaries.py => test_checks.py} (71%) create mode 100644 tests/fortran/infrastructure/codegen/test_docstrings.py create mode 100644 tests/fortran/infrastructure/codegen/test_generator.py create mode 100644 tests/fortran/infrastructure/codegen/test_package.py create mode 100644 tests/fortran/infrastructure/codegen/test_plan.py rename tests/fortran/infrastructure/codegen/{test_wrapper_plan_validation.py => test_planner.py} (50%) create mode 100644 tests/fortran/infrastructure/codegen/test_pyi_printer.py delete mode 100644 tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py rename tests/fortran/infrastructure/codegen/{test_backend_foundations.py => test_source_printers.py} (93%) delete mode 100644 tests/fortran/infrastructure/codegen/test_wrapper_assembly.py rename tests/{shared => fortran/infrastructure}/naming/test_policy.py (100%) create mode 100644 tests/fortran/infrastructure/pipeline/test_wrapper_artifacts.py create mode 100644 tests/fortran/infrastructure/semantics/test_native_array_handles.py rename tests/fortran/infrastructure/{policy/test_policy_defaults_and_validation.py => semantics/test_ownership.py} (73%) rename tests/fortran/infrastructure/{policy/test_accessor_and_storage_policy.py => semantics/test_policy_completion.py} (67%) rename tests/fortran/infrastructure/{policy => semantics}/test_wrapper_policy.py (98%) rename tests/{shared => fortran/infrastructure}/types/test_mapping_report.py (98%) rename tests/{shared => fortran/infrastructure}/types/test_numpy.py (100%) rename tests/{shared/architecture/test_visitor_protocol.py => fortran/infrastructure/utilities/test_class_visitor.py} (100%) rename tests/{shared => fortran/infrastructure}/utilities/test_strings.py (100%) delete mode 100644 tests/shared/README.md delete mode 100644 tests/shared/architecture/test_dependency_boundaries.py delete mode 100644 tests/shared/architecture/test_package_structure.py delete mode 100644 tests/shared/architecture/test_test_suite_layout.py delete mode 100644 tests/shared/docs/test_structure.py create mode 100644 tests/tools/README.md rename tests/{shared => }/tools/test_benchmark_host.py (100%) rename tests/{shared => }/tools/test_build_time_benchmark.py (100%) rename tests/{shared => }/tools/test_check_benchmark_regression.py (100%) rename tests/{shared => }/tools/test_check_radon_policy.py (100%) rename tests/{shared => }/tools/test_check_static_analysis_versions.py (100%) rename tests/{shared => }/tools/test_generate_performance_docs.py (100%) rename tests/{shared => }/tools/test_print_pytest_failures.py (100%) rename tests/{shared => }/tools/test_run_fortran_toolchain_lane.py (100%) rename tests/{shared => }/tools/test_runtime_benchmark.py (100%) rename tests/{shared => }/tools/test_warm_real_library_native_cache.py (100%) create mode 100644 tests/workflows/README.md create mode 100644 tests/workflows/test_release.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3e5522f6e..822d4c441 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -60,10 +60,11 @@ jobs: -o junit_family=legacy \ --junitxml="$RUNNER_TEMP/pytest-suite-results.xml" \ -m "not real_library and not toolchain_smoke" \ - tests/architecture \ tests/c \ + tests/docs \ tests/fortran \ - tests/shared + tests/tools \ + tests/workflows - name: Summarize failed pytest nodes if: failure() shell: bash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c709badc4..2ad96136d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -134,7 +134,7 @@ jobs: path: . - name: Run documentation tests - run: python -m pytest -q tests/shared/docs + run: python -m pytest -q tests/docs - name: Build reviewed documentation run: python -m mkdocs build --strict diff --git a/.github/workflows/merge-validation.yml b/.github/workflows/merge-validation.yml index 1e55ee552..c7f9fa527 100644 --- a/.github/workflows/merge-validation.yml +++ b/.github/workflows/merge-validation.yml @@ -299,10 +299,11 @@ jobs: -o junit_family=legacy \ --junitxml="$RUNNER_TEMP/pytest-suite-results.xml" \ -m "not real_library and not toolchain_smoke" \ - tests/architecture \ tests/c \ + tests/docs \ tests/fortran \ - tests/shared + tests/tools \ + tests/workflows - name: Run tests with project coverage if: ${{ matrix.coverage }} shell: bash @@ -322,10 +323,11 @@ jobs: -o junit_family=legacy \ --junitxml="$RUNNER_TEMP/pytest-suite-results.xml" \ -m "not real_library and not toolchain_smoke" \ - tests/architecture \ tests/c \ + tests/docs \ tests/fortran \ - tests/shared + tests/tools \ + tests/workflows - name: Summarize failed pytest nodes if: failure() shell: bash @@ -422,10 +424,11 @@ jobs: -o junit_family=legacy \ --junitxml="$RUNNER_TEMP/pytest-macos-suite-results.xml" \ -m "not real_library and not toolchain_smoke" \ - tests/architecture \ tests/c \ + tests/docs \ tests/fortran \ - tests/shared + tests/tools \ + tests/workflows - name: Summarize failed pytest nodes if: failure() shell: bash @@ -646,7 +649,7 @@ jobs: name: performance-snapshot path: . - name: Run documentation tests - run: python -m pytest -q tests/shared/docs + run: python -m pytest -q tests/docs - name: Build reviewed documentation run: python -m mkdocs build --strict diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 779e6c76f..28735ee60 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,10 +62,11 @@ jobs: -o junit_family=legacy \ --junitxml="$RUNNER_TEMP/pytest-suite-results.xml" \ -m "not real_library and not toolchain_smoke" \ - tests/architecture \ tests/c \ + tests/docs \ tests/fortran \ - tests/shared + tests/tools \ + tests/workflows - name: Summarize failed pytest nodes if: failure() shell: bash @@ -144,10 +145,11 @@ jobs: -o junit_family=legacy \ --junitxml="$RUNNER_TEMP/pytest-macos-suite-results.xml" \ -m "not real_library and not toolchain_smoke" \ - tests/architecture \ tests/c \ + tests/docs \ tests/fortran \ - tests/shared + tests/tools \ + tests/workflows - name: Summarize failed pytest nodes if: failure() shell: bash diff --git a/AGENTS.md b/AGENTS.md index c9f04b729..42bd372c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ For documentation-only changes that do not modify executable Python code, runtime behavior, build configuration, or test logic, do not run the complete static-analysis suite by default. Run the focused documentation checks and whitespace check instead: -- `python3 -m pytest -q tests/shared/docs/test_examples.py tests/shared/docs/test_structure.py` +- `python3 -m pytest -q tests/docs` - `git diff --check` Run the complete static-analysis suite when code, tests, build behavior, or tooling configuration changes, or when explicitly requested for pre-merge or diff --git a/CHANGELOG.md b/CHANGELOG.md index f3221b787..75ebddddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ release tags add a leading `v` to the package version. ### Changed +- Moved documentation and maintainer-tool tests to `tests/docs/` and + `tests/tools/`, removed the generic `tests/shared/` bucket, and mirrored + internal tests by production package with narrower support helpers; removed + recursive layout-policing tests that froze maintainer organization, retaining + exceptional release safety under `tests/workflows/`. - Simplified the documented DGESV validation and the LAPACK test suite to use explicit NumPy Fortran-order copies, with documented numerical-test helper conventions. @@ -44,7 +49,7 @@ release tags add a leading `v` to the package version. - Refreshed the README and website around the canonical **PRIK — Python Runtime Interop Kit** identity, with a concise FAQ, a fair PRIK-versus-f2py guide, clearer array guidance, and searchable real-library - examples. + examples, including a four-library capability and validation summary. - Hardened preprocessing, compiler-derived type probes, semantic policy completion, and multi-source build reporting so unsupported contracts fail earlier with clearer diagnostics. diff --git a/README.md b/README.md index 1d458ea06..265f2e0e7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ and Pythonic APIs.** PRIK generates native Python bindings from Fortran projects, producing importable extensions and editable `.pyi` contracts for Pythonic APIs. +[![Tests](https://github.com/PyNumLab/prik/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/PyNumLab/prik/actions/workflows/tests.yml) +[![Static Analysis](https://github.com/PyNumLab/prik/actions/workflows/static-analysis.yml/badge.svg?branch=main)](https://github.com/PyNumLab/prik/actions/workflows/static-analysis.yml) +[![codecov](https://codecov.io/gh/PyNumLab/prik/graph/badge.svg?token=QZRRCS5YO6)](https://codecov.io/gh/PyNumLab/prik) + It preserves modules, derived types, arrays, callbacks, and native behavior so you can shape the resulting API without writing low-level binding code. @@ -24,23 +28,12 @@ semantic IR, emits editable `.pyi` interfaces, and reports unsupported or incomplete contracts before code generation. PRIK_C_DOCS_END --> -[![Tests](https://github.com/PyNumLab/prik/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/PyNumLab/prik/actions/workflows/tests.yml) -[![Static Analysis](https://github.com/PyNumLab/prik/actions/workflows/static-analysis.yml/badge.svg?branch=main)](https://github.com/PyNumLab/prik/actions/workflows/static-analysis.yml) -[![codecov](https://codecov.io/gh/PyNumLab/prik/graph/badge.svg?token=QZRRCS5YO6)](https://codecov.io/gh/PyNumLab/prik) - [Read the documentation](https://pynumlab.github.io/prik/) for installation, the user guide, examples, and reference material. -For a complete real-library example, see the -[155-routine BLAS correctness project](examples/blas/README.md), which builds -the same Reference BLAS sources with PRIK and f2py and checks both against -independent numerical expectations. The -[LAPACK correctness project](examples/lapack/README.md) wraps the complete -Reference LAPACK implementation corpus once and validates the reviewed 127 -SciPy-backed double-precision routines in the dedicated CI lane. - ## Contents +- [Proven on real libraries](#proven-on-real-libraries) - [See it in action](#see-it-in-action) - [Key Features](#key-features) - [Performance](#performance) @@ -52,6 +45,22 @@ SciPy-backed double-precision routines in the dedicated CI lane. - [License](#license) - [Documentation](#documentation) +## Proven on real libraries + +The maintained projects build real numerical libraries with PRIK and validate +their Python behavior, not just whether the generated wrapper compiles. + +| Project | Validated surface | Capabilities demonstrated | +| --- | --- | --- | +| [BLAS](examples/blas/README.md) | All 155 discovered routines | Scalar, vector, and matrix operations; increments and leading dimensions; in-place updates; independent expectations and f2py comparisons | +| [LAPACK](examples/lapack/README.md) | Complete implementation corpus with 127 reviewed double-precision routines | Linear solves, factorizations, eigenproblems, singular values, work arrays, and large multi-source linking | +| [FFTPACK](examples/fftpack/README.md) | All 31 public procedures | Fourier, cosine, and sine transforms; low-level workspaces; in-place arrays; allocatable results; NumPy and SciPy oracles | +| [MINPACK](examples/minpack/README.md) | All 22 public procedures | Python callbacks; nonlinear and least-squares solvers; Jacobian and workspace writeback; immutable module constants | + +Together they exercise arrays, callbacks, workspaces, in-place mutation, +allocatable results, module constants, and multi-file linking. The dedicated +Real Libraries CI lane builds and tests all four projects. + The complete example below builds with one command: ```bash @@ -114,12 +123,20 @@ shows the available edits. ## Key Features -- Fortran modules exposed as Python namespaces and derived types as classes -- NumPy arrays with explicit dtype, shape, and layout checks -- Allocatable and pointer arrays with explicit lifetime operations -- Immediate Python callbacks and overloaded interfaces -- Editable `.pyi` contracts and readable generated docstrings -- Early, clear errors when a boundary cannot be wrapped +- **Native APIs that feel like Python.** Fortran modules become Python + namespaces, while derived types become classes with fields and methods. +- **First-class NumPy array interop.** Pass ordinary NumPy arrays to native + procedures, including multidimensional and in-place data, with generated + dtype, shape, layout, and mutability handling at the language boundary. +- **Managed access to native memory.** Expose allocatable and pointer arrays + without hiding their ownership, lifetime, allocation, or release operations. +- **Python callbacks and native overloads.** Pass Python callables into Fortran + and expose generic interfaces as familiar Python overloads. +- **Generated APIs you can reshape.** Edit the generated `.pyi` contract to + rename, hide, reorganize, or overload the public interface, backed by readable + generated docstrings. +- **Unsupported contracts fail before the build.** PRIK identifies the exact + boundary and reason before attempting code generation or compilation. ## Performance diff --git a/docs/developer/development-workflow.md b/docs/developer/development-workflow.md index 3ea48a308..7c2b56b34 100644 --- a/docs/developer/development-workflow.md +++ b/docs/developer/development-workflow.md @@ -101,7 +101,7 @@ PRIK_C_DOCS_END --> ### Automatically Verify Markdown Examples -`tests/shared/docs/test_examples.py` executes explicitly marked +`tests/docs/test_examples.py` executes explicitly marked `bash` CLI examples and `python` API snippets from `README.md` and Markdown files under `docs/`. Bash examples must be `python3 -m prik` commands; the test replaces `python3` with the active test interpreter and runs them without a shell. It rejects @@ -169,7 +169,7 @@ and compare its complete output. Run the documentation checks directly: ```bash -PYTHONPATH=. python3 -m pytest -q tests/shared/docs/test_examples.py +PYTHONPATH=. python3 -m pytest -q tests/docs/test_examples.py ``` ## References @@ -208,15 +208,15 @@ implementation files. | Fortran parse output | `prik/parsers/fortran/parser.py`, `prik/parsers/fortran/models.py`, `prik/parsers/fortran/lexer.py` | `tests/fortran/source_parsing/parsing/`, `tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py`, `tests/fortran/source_parsing/parsing/test_error_handling.py` | | CLI stage selection and output | `prik/cli.py`, `prik/parsers/fortran/cli.py` | `tests/fortran/command_line_interface/pipeline/` | | Fortran target type probing and cache | `prik/probes/fortran_types.py` | `tests/fortran/data_types/probes/test_fortran_type_probes.py` | -| Generated target datatype mapping examples | `prik/probes/report.py` | `tests/shared/types/test_mapping_report.py`, `tests/shared/docs/test_examples.py` | +| Generated target datatype mapping examples | `prik/probes/report.py` | `tests/fortran/infrastructure/types/test_mapping_report.py`, `tests/docs/test_examples.py` | | Fortran to semantic IR | `prik/semantics/fortran2ir.py`, `prik/semantics/models.py` | `tests/fortran/semantic_ir/semantics/` | | `.pyi` printing | `prik/codegen/printers/pyi_printer.py` | `tests/fortran/semantic_pyi_format/pipeline/`, `tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py` | | `.pyi` parsing/loading/editing | `prik/parsers/pyi/parser.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py` | `tests/fortran/semantic_pyi_format/` | -| Semantic policy completion | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py` | `tests/fortran/infrastructure/policy/` and feature-local `policy/` directories | +| Semantic policy completion | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py` | `tests/fortran/infrastructure/semantics/` and feature-local `policy/` directories | | Fortran wrapper orchestration | `prik/pipeline/build.py` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py`, `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py` | | Wrapper planning, owner-local errors, and direct lowering | `prik/codegen/plan.py`, `prik/codegen/planner.py`, `prik/codegen/generator.py` | `tests/fortran/infrastructure/codegen/`, feature-local `codegen/` stages | | Native compilation and binding support | `prik/compiling/`, `prik/binding_support/` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py`, `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py` | -| Executable Markdown examples | `README.md`, `docs/*.md` | `tests/shared/docs/test_examples.py` | +| Executable Markdown examples | `README.md`, `docs/*.md` | `tests/docs/test_examples.py` | | `prik/pipeline/` | Source preprocessing, semantic `.pyi` loading, and wrapper build orchestration | `preprocessing.py`, `pyi.py`, `build.py` | preprocessing, `.pyi`, and wrapper build tests | | `prik/probes/` | Compiler-derived target facts plus mapping reports | `fortran_types.py`, `report.py` | target probe and type mapping report tests | | `prik/runtime/` | Python runtime objects consumed by generated extensions | `handles.py` | runtime handle and wrapper runtime tests | -| `prik/types/` | Semantic-to-Python ecosystem type mappings | `numpy.py` | `tests/shared/types/test_numpy.py` | +| `prik/types/` | Semantic-to-Python ecosystem type mappings | `numpy.py` | `tests/fortran/infrastructure/types/test_numpy.py` | | `prik/parsers/` | Public namespace for language and semantic `.pyi` frontends | child parser packages | `tests/fortran/source_parsing/parsing/`, `tests/c/parsing/`, `tests/fortran/semantic_pyi_format/parsing/` | | `prik/parsers/fortran/` | Fortran lexer, recursive parser, models, type resolver, and parser CLI helpers | `parser.py`, `lexer.py`, `models.py`, `type_resolver.py`, `cli.py` | `tests/fortran/source_parsing/parsing/`, `docs/developer/fortran-parser-reference.md` | | `prik/compiling/` | Native compile objects, compiler command execution, shared-library linking, and native support installation; wrapper build orchestration lives in `prik/pipeline/build.py` | `objects.py`, `compilers.py`, `compiler_profiles.py`, `native_support.py` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py`, `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py` | | `prik/binding_support/` | Bundled header-only native binding support copied into generated wrapper builds | support header | wrapper build tests | -| `prik/utilities/` | Small shared Python utilities | `strings.py`, `visitor.py` | tests that exercise callers | +| `prik/utilities/` | Small shared Python utilities | `strings.py`, `visitor.py` | `tests/fortran/infrastructure/utilities/` and tests that exercise callers | These files are the maintained source-navigation anchors. If ownership moves, update this table, the package README files, and the mechanical checks in -`tests/shared/docs/test_structure.py` in the same change. +`tests/docs/test_reference_and_source_map.py` in the same change. | Hotspot | Owns | | --- | --- | diff --git a/docs/developer/testing-strategy.md b/docs/developer/testing-strategy.md index 6dc887a38..a53934a60 100644 --- a/docs/developer/testing-strategy.md +++ b/docs/developer/testing-strategy.md @@ -19,16 +19,11 @@ record the active migration gates in Tests first answer which native input contract they exercise: -- `tests/architecture/` owns meta-tests of the test suite's structure, - evidence indexes, and selections; +- `tests/docs/` owns documentation metadata, navigation, examples, and content; +- `tests/tools/` owns maintainer commands and CI support scripts; +- `tests/workflows/` owns exceptional automation-safety checks; - `tests/fortran/` owns Fortran input and semantic `.pyi` wrapper behavior; -- `tests/c/` owns C input-language inspection behavior; and -- `tests/shared/` owns behavior that neither imports nor selects a native - language. - -The architecture root is deliberately outside the language trees it validates. -Use a language subdirectory such as `tests/architecture/fortran/` when a -meta-invariant is language-specific. +- `tests/c/` owns C input-language inspection behavior. Within Fortran, user-visible behavior is feature first and pipeline stage second: @@ -173,12 +168,46 @@ construction. ## Ownership discipline -Every maintained test and checked fixture has one final language-first owner. -The only non-language owner is `tests/architecture/`, for tests of the suite's -own structure and evidence system. Do not use it for product behavior or as a -stage-first compatibility root. Do not add forwarding fixtures, import -aliases, or fixture path fallbacks. Cross-feature mechanics require an explicit -infrastructure owner; language-neutral product checks require a shared owner. +Every maintained test and checked fixture has one behavioral owner. Directory +layout, exact file inventories, and the current organization of the tests are +maintainer conventions rather than executable product contracts. Do not add +tests whose only purpose is to make an intentional reorganization fail. Add a +structural check only for a concrete, unusually costly risk that cannot be +protected by a behavior test. Cross-feature product mechanics require an +explicit infrastructure owner; documentation and maintainer tools use their +named top-level feature owners. + +A test has one of two navigation shapes after language ownership is known: + +- user-visible behavior lives under + `tests/fortran///`; or +- a genuinely internal mechanism lives under + `tests/fortran/infrastructure//test_.py`. + +For example, semantic-policy internals use +`infrastructure/semantics/test_ownership.py` and +`test_policy_completion.py`; wrapper internals use +`infrastructure/codegen/test_plan.py`, `test_planner.py`, and +`test_generator.py`. Other internal owners mirror `prik/compiling/`, +`prik/contracts/`, `prik/pipeline/`, `prik/runtime/`, and the remaining source +packages when they have real internal tests. Do not create empty mirror +directories or combine multiple production owners in generic backend or policy +collection modules. A retained direct-execution example +under `if __name__ == "__main__"` is maintained by the same dedicated test +module. + +Documentation tests are grouped by invariant rather than by Markdown page. +Generic metadata, navigation, source-marker, link, and executable-example +validators stay parameterized over applicable pages. Page-specific content +contracts live in a module named for the documentation area they protect. +Workflow checks belong under `tests/workflows/` only when they +protect concrete behavior or release safety; they should not duplicate or +freeze the current CI organization. + +Test support contains reusable construction or assertion behavior, not an +alternate import surface for production code. Tests import `pytest`, Python +standard-library names, and production symbols from their real owners; support +modules do not re-export them merely to shorten imports. ## Required verification diff --git a/docs/maintainer/roadmap/documentation-content-checklist.md b/docs/maintainer/roadmap/documentation-content-checklist.md index 8191c1cbc..4824e037d 100644 --- a/docs/maintainer/roadmap/documentation-content-checklist.md +++ b/docs/maintainer/roadmap/documentation-content-checklist.md @@ -41,7 +41,7 @@ these are true: reserve the full static-analysis suite for code, tests, build/tooling changes, or explicit pre-merge verification. - [ ] User, Developer, and Maintainer lane entry points, `mkdocs.yml`, related - front matter, and `tests/docs/test_structure.py` stay + front matter, and `tests/docs/test_navigation.py` stay synchronized. ## Open Documentation Queue diff --git a/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md b/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md index 425f06fff..ae0e74525 100644 --- a/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md +++ b/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md @@ -107,7 +107,7 @@ tests/ functions_and_classes/ calls_and_results/ infrastructure/ - policy/ + semantics/ codegen/ c/ README.md @@ -118,15 +118,8 @@ tests/ conversion/ pipeline/ fixtures/ - shared/ - architecture/ - cli/ - contracts/ - docs/ - naming/ - tools/ - types/ - utilities/ + docs/ + tools/ ``` Each documentation feature may contain only the stages it actually needs: @@ -153,9 +146,9 @@ behavior and public cross-feature capabilities do not. Fortran contract. - [x] `tests/c/` means C is the input language. It does not own generated C used to implement a Fortran wrapper. -- [x] `tests/shared/` contains only language-neutral behavior. A shared test or - fixture must not select C versus Fortran through a parameter, conditional, - suffix, compiler name, or fallback path. +- [x] Documentation and maintainer-tool tests have named top-level owners; + internal language-neutral mechanics mirror their `prik/` package under + `tests/fortran/infrastructure/`. - [x] C receives a mechanical quarantine only. Its behavior is not redesigned in this project. - [x] Fortran receives the documentation-led behavioral cleanup. @@ -233,8 +226,8 @@ Rules: ### CLI ownership -- [x] Generic argument parsing and output formatting belong in - `tests/shared/cli/`. +- [x] Public argument parsing and output formatting belong in the owning input + language's command-line feature. - [x] Cross-feature Fortran command contracts belong in `tests/fortran/command_line_interface/pipeline/`. - [x] A CLI test that builds, imports, calls, and verifies a Fortran extension @@ -307,8 +300,8 @@ Each testable documentation claim gets one row with this minimum schema: - [x] Record source build, generated-`.pyi` replay, edited-`.pyi`, and source-free native-artifact evidence separately when the documentation claims each route. -- [x] Make architecture tests resolve documentation links and collected node - IDs. +- [x] Resolve documentation links and collected node IDs during the migration + review. - [x] Reject deletion of the last evidence for a contract row. ### Temporary pytest migration ledger @@ -388,7 +381,7 @@ directory. Audit and place every artifact beside its final behavioral owner. | `tests/data/fortran/blas/` and `lapack/` | `examples/blas/native/` and `examples/lapack/native/` | | Parser regressions extracted from SciFortran | `tests/fortran/source_parsing/parsing/test_real_world_interaction_regressions.py` | | Parser source/JSON pairs | Beside their parser owner | -| Language-neutral `.pyi` syntax | `tests/shared/contracts/` | +| Language-neutral `.pyi` syntax | `tests/fortran/semantic_pyi_format/` | | Fortran `.pyi` build fixtures | `tests/fortran/semantic_pyi_format/{pipeline,end_to_end}/fixtures/` | | Generated contract goldens | Beside their generation/package-shape owner | | Edited contracts | `tests/fortran/pyi_contracts//end_to_end/fixtures/` | @@ -1261,7 +1254,7 @@ before changing compiler product behavior. - [x] BLAS, LAPACK, parser-regression, and contract content exists only beneath its final owner; no SciFortran snapshot remains. - [x] Every permanent contract row resolves to final collected nodes. -- [x] Collect `tests/fortran/`, `tests/c/`, and `tests/shared/` independently; +- [x] Collect `tests/fortran/`, `tests/c/`, `tests/docs/`, and `tests/tools/` independently; run the local Fortran verification with `-m "not real_library"`. - [x] Run the new suites alone under the same CI-equivalent line-and-branch coverage procedure used for the baseline. @@ -1295,14 +1288,13 @@ legacy golden at its final owner made its two focused printer nodes pass; the fixture-only correction does not change executable coverage. The coverage run otherwise recorded 3,916 passes, 12 documented skips, and 10 deselections. -The clean final ordinary gate collects 3,941 repository nodes. After moving -suite-structure meta-tests outside the language trees, the independent roots -collect 22 architecture nodes, 498 C nodes, 1,275 shared nodes, and 2,146 -Fortran nodes: 2,144 are ordinary/smoke nodes and exactly two are the -BLAS/LAPACK `real_library` rows. The unchanged ordinary selection records 3,919 -passes, 12 documented skips, and 10 deselections in 643.60 seconds. Strict -smoke separately records exactly eight passes in 18.49 seconds, for 662.09 -seconds across the two executed selections. LAPACK was collected but not run. +The historical migration gate collected 3,941 repository nodes: 498 C nodes, +1,275 shared nodes, 2,146 Fortran nodes, and 22 temporary layout-validation +nodes. The unchanged ordinary selection recorded 3,919 passes, 12 documented +skips, and 10 deselections in 643.60 seconds. Strict smoke separately recorded +exactly eight passes in 18.49 seconds, for 662.09 seconds across the two +executed selections. LAPACK was collected but not run. The temporary layout +checks were later removed rather than freezing the completed organization. An uncached compiler-proxy audit across those same two selections records 1,266 real compiler executions: 562 compilations (382 Fortran and 180 C), 186 @@ -1375,7 +1367,7 @@ Rules: or any setup/call/teardown report skips or xfails; and - [x] emits a deterministic collection report. -`tests/architecture/fortran/test_smoke_selection.py`: +The smoke selection was reviewed to: - [x] validates marker registration and exact collected nodes; - [x] validates contract-ledger membership; @@ -1455,7 +1447,7 @@ python -m pytest -q tests/fortran -m toolchain_smoke \ --prik-fortran-compiler= --require-toolchain-smoke ``` -`tests/architecture/fortran/test_ci_toolchain_lanes.py` verifies: +The runner and live CI lanes provide the following checks: - [x] every compiler lane includes its profile tests; - [ ] every macOS lane includes macOS platform tests; @@ -1479,7 +1471,7 @@ CI. Use explicit `matrix.include` records only. - [ ] Canonical Linux x86-64, middle Python, GFortran: all non-corpus Fortran stage tests, ordinary non-real-library Fortran end-to-end, the mechanically - preserved C suite, shared tests, and canonical coverage. + preserved C suite, documentation and tool tests, and canonical coverage. - [ ] Oldest Python, Linux x86-64, GFortran: install/import, selected Python-facing stage tests, and toolchain smoke. - [ ] Newest Python, Linux x86-64, GFortran: the same compatibility selection. @@ -1495,7 +1487,7 @@ CI. Use explicit `matrix.include` records only. smoke. - [ ] Linux x86-64, middle Python, NVIDIA nvfortran: profile tests, focused CLI, and smoke. -- [ ] Linux ARM64, middle Python, GFortran: architecture tests, focused CLI, and +- [ ] Linux ARM64, middle Python, GFortran: tool-runner tests, focused CLI, and smoke. - [ ] Linux x86-64, middle Python, GFortran with ASan/UBSan: compatible runtime selection and memory-safety checks. diff --git a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md index 216bd669a..b2ca7faf2 100644 --- a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md +++ b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md @@ -100,7 +100,7 @@ argument handoff from the native barrier action. `prik.utilities.visitor.ClassVisitor` and configured `_` handlers instead of parallel visitor implementations or local `isinstance` dispatch ladders. -- [x] Structural evidence lives in `tests/architecture/test_visitor_protocol.py` +- [x] Structural evidence lives in `tests/fortran/infrastructure/utilities/test_class_visitor.py` and `tests/semantics/policy/`; runtime evidence covers scalar value/address projection, rank-0 scalar storage, arrays, strings, raw addresses, and wrapper instances through focused `tests/wrapper/fortran/` @@ -463,7 +463,7 @@ PRIK_C_DOCS_END --> edited contracts. Evidence: `docs/user/reference/pyi-contracts/`, `docs/user/reference/fortran-wrapper.md`, and - `tests/docs/test_structure.py`. + `tests/docs/test_user_content.py`. - [x] Edited contracts can remove a class, method, generated constructor, class member, and individual overload candidate from the Python API. They can also add renamed `@bind(...)` declarations and a renamed module overload group diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index cf7a2eb44..fefb44ae3 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -4809,7 +4809,7 @@ maintainability reports, and `git diff --check` all passed. ## Verification - [x] Documentation changes run - `python3 -m pytest -q tests/docs/test_examples.py tests/docs/test_structure.py` + `python3 -m pytest -q tests/docs` and `git diff --check`. - [x] Wrapper-plan code changes run the affected existing `tests/wrapper` nodes, the minimal intermediate contract tests required above, and the required diff --git a/docs/user/language-support/feature-matrix.md b/docs/user/language-support/feature-matrix.md index 7dbb8a03a..ecc15f54a 100644 --- a/docs/user/language-support/feature-matrix.md +++ b/docs/user/language-support/feature-matrix.md @@ -69,7 +69,7 @@ PRIK_C_DOCS_END --> | Semantic `.pyi` wrapper builds from explicit native artifacts | Partially supported | [Semantic `.pyi` contracts](../examples/recipes/semantic-pyi-contracts.md), [`.pyi` format](../reference/semantic-pyi-format.md) | [`.pyi` build route](../../developer/source-map.md#layer-to-layer-route) | [format and authoritative-input tests](../../../tests/fortran/semantic_pyi_format/), [multi-source contract tests](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py), [native build plan tests](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py) | Current runtime parity is limited; source/generated/modified multi-source package parity is covered, and broader parity remains tracked in the checklist. | | Scalar inheritance and polymorphic dispatch | Partially supported | [Inheritance and polymorphism](../reference/fortran-wrapper.md#inheritance-and-polymorphism) | [Class lowering route](../../developer/source-map.md#common-change-routes) | [Inheritance tests](../../../tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py) | Polymorphic results, mutable dummies, arrays, allocatable/pointer scalars, and `class(*)` are blocked. | | Assumed-size, assumed-rank, and lower-bound array contracts | Partially supported | [Arrays](../guide/arrays.md) | [Array bridge route](../../developer/source-map.md#common-change-routes) | [Assumed-rank tests](../../../tests/fortran/arrays/end_to_end/test_assumed_rank_arrays.py) | Assumed type and derived-type arrays remain blocked. Character arrays require fixed-width NumPy bytes dtype. | -| Generated reference pages for modules, functions, and classes | Partially supported | [Reference index](../reference/index.md) | [Source map](../../developer/source-map.md) | [Documentation structure checks](../../../tests/shared/docs/test_structure.py), [semantic contract tests](../../../tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py) | Maintained manual references exist for generated functions, modules, classes, and generated file contracts; automated reference inventory generation has not been selected. | +| Generated reference pages for modules, functions, and classes | Partially supported | [Reference index](../reference/index.md) | [Source map](../../developer/source-map.md) | [Documentation reference checks](../../../tests/docs/test_reference_and_source_map.py), [semantic contract tests](../../../tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py) | Maintained manual references exist for generated functions, modules, classes, and generated file contracts; automated reference inventory generation has not been selected. | | Feature | Status | User docs | Source owner | Evidence | Limitations | | --- | --- | --- | --- | --- | --- | | Full semantic `.pyi` parity across all wrapper scenarios | Planned | [Semantic `.pyi` format](../reference/semantic-pyi-format.md) | [`.pyi` route](../../developer/source-map.md#layer-to-layer-route) | [semantic `.pyi` feature tests](../../../tests/fortran/semantic_pyi_format/) | Only the documented implemented subset is supported. | -| MPI examples and distribution constraints | Not implemented | [MPI example](../examples/mpi-example.md) | [Planned examples](../examples/index.md) | [Documentation structure checks](../../../tests/shared/docs/test_structure.py) | No support contract or runnable evidence exists yet. | +| MPI examples and distribution constraints | Not implemented | [MPI example](../examples/mpi-example.md) | [Planned examples](../examples/index.md) | [Documentation navigation checks](../../../tests/docs/test_navigation.py) | No support contract or runnable evidence exists yet. | diff --git a/docs/user/reference/configuration-files.md b/docs/user/reference/configuration-files.md index ad0abb245..c3d36d7ba 100644 --- a/docs/user/reference/configuration-files.md +++ b/docs/user/reference/configuration-files.md @@ -160,7 +160,7 @@ from the appropriate area index and `mkdocs.yml` navigation. Documentation-only changes normally run: ```bash -python3 -m pytest -q tests/shared/docs +python3 -m pytest -q tests/docs python3 -m mkdocs build --strict git diff --check ``` @@ -177,9 +177,9 @@ source-build Makefile behavior by [`test_build_modes.py`](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py). Tooling configuration is covered by -[`test_documentation_structure.py`](../../../tests/shared/docs/test_structure.py), -[`test_documentation_examples.py`](../../../tests/shared/docs/test_examples.py), and -[`test_check_static_analysis_versions.py`](../../../tests/shared/tools/test_check_static_analysis_versions.py). +[`test_reference_and_source_map.py`](../../../tests/docs/test_reference_and_source_map.py), +[`test_examples.py`](../../../tests/docs/test_examples.py), and +[`test_check_static_analysis_versions.py`](../../../tests/tools/test_check_static_analysis_versions.py). When a generated file contract changes, update this page with the CLI reference, Python API reference, shared-library build guide, and wrapper tests that prove diff --git a/prik/semantics/README.md b/prik/semantics/README.md index a2913e680..525b32dbf 100644 --- a/prik/semantics/README.md +++ b/prik/semantics/README.md @@ -8,19 +8,19 @@ editable `.pyi` files, policy completion, and wrapper code generation. | File | Owns | | --- | --- | | `models.py` | Semantic IR dataclasses and core model metadata. | -| `../semantic_metadata.py` | Cross-stage semantic metadata keys consumed after `.pyi`, C, or Fortran conversion. | +| `metadata.py` | Cross-stage semantic metadata keys consumed after `.pyi`, C, or Fortran conversion. | | `fortran2ir.py` | Fortran parser facts to semantic modules. | | `c2ir.py` | C parser facts to semantic modules. | | `pyi2ir.py` | User-editable semantic `.pyi` AST conversion and validation. | -| `../pyi_pipeline.py` | Combined `.pyi` text/file/path-set conversion and external-type reconciliation. | +| `../pipeline/pyi.py` | Combined `.pyi` text/file/path-set conversion and external-type reconciliation. | | `pyi_metadata.py` | Semantic `.pyi` loader workflow metadata. | | `native_contract.py` | Source-free native ABI and placement validation. | | `policy_completion.py` | Complete ownership, transfer, destruction, mutability/writeback, projection, nullability, release, storage, Python-barrier, native-barrier, and accessor decisions after full signatures are known. | -| `ir2ast.py` | Semantic IR to codegen AST lowering for wrapper generation; consumes completed policies. | +| `../codegen/planner.py` | Converts completed semantic policy into the typed wrapper plan consumed by code generation. | ## Declaration Expressions -`utilities/declaration_expressions.py` owns the shared declaration-expression +`../utilities/declaration_expressions.py` owns the shared declaration-expression grammar, normalization, callable/reference discovery, native-style rendering, and deterministic integer evaluation. Semantic conversion does not evaluate an arbitrary native specification function. Instead, it records the function's @@ -46,11 +46,11 @@ C parser facts, Fortran parser facts, or parsed .pyi AST -> semantic modules -> semantic policy completion -> complete storage, Python-barrier, and native-barrier policy - -> wrapper planning or codegen AST + -> typed wrapper planning ``` -`ir2ast.py` is the boundary where semantic contracts become generated-wrapper -implementation details. Object kind, ownership, transfer, destruction, +`../codegen/planner.py` is the boundary where semantic contracts become typed +wrapper implementation plans. Object kind, ownership, transfer, destruction, mutability/writeback, result projection, nullability, release responsibility, contract/boundary storage modes, Python-barrier action, and native-barrier action must be completed before this boundary by `policy_completion.py` using @@ -80,7 +80,7 @@ Completed module-variable policy selects direct binding materialization for a literal constant and a read-only native getter for a symbolic numeric source parameter. -`ir2ast.py`, bridges, and bindings consume those decisions +The planner, bridges, and bindings consume those decisions instead of making local policy guesses. Bridge and binding dispatch is strict: an unregistered barrier action or object-kind/action pair is an error rather than a fallback. Model-node dispatch uses `prik.utilities.visitor.ClassVisitor` and the @@ -103,4 +103,4 @@ completion remains the next shared stage after those converters produce - Pipeline map: `docs/maintainer/internal-architecture/pipeline-map.md` - Semantic tests: `tests/fortran/semantic_ir/semantics/` - `.pyi` tests: `tests/fortran/semantic_pyi_format/` -- Wrapper behavior that reaches `ir2ast.py`: `tests/fortran/` +- Wrapper behavior that reaches the typed plan: `tests/fortran/` diff --git a/tests/README.md b/tests/README.md index e2160a5cc..95957536d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,19 +7,22 @@ organized by documented feature and pipeline stage: tests/fortran/// ``` -Meta-tests that validate the test suite itself live under -`tests/architecture/`; they are outside the tree whose structure they enforce. -Only genuinely language-neutral product behavior belongs under `tests/shared/`. +Documentation is the top-level `tests/docs/` feature. Only other genuinely +internal product behavior mirrors its production package below +`tests/fortran/infrastructure/`. Maintainer tooling has the independent +`tests/tools/` owner, while exceptional automation-safety checks live under +`tests/workflows/`. Generated C and CPython binding code used by a Fortran wrapper remains evidence for the Fortran input contract; `tests/c/` means C is the user-owned input language. | Owner | Contract | Focused command | | --- | --- | --- | -| [`tests/architecture/`](architecture/README.md) | Test-suite ownership, evidence-ledger, selection, and collection invariants | `python3 -m pytest -q tests/architecture` | +| [`tests/docs/`](docs/README.md) | Documentation metadata, navigation, executable examples, publication, content journeys, and source-map synchronization | `python3 -m pytest -q tests/docs` | | [`tests/fortran/`](fortran/README.md) | Fortran input, semantic `.pyi` wrapper contracts, generated bridge/binding behavior, and Fortran runtime features | `python3 -m pytest -q tests/fortran` | | [`tests/c/`](c/README.md) | C input-language inspection behavior | `python3 -m pytest -q tests/c` | -| [`tests/shared/`](shared/README.md) | Behavior that neither imports nor selects a native input language | `python3 -m pytest -q tests/shared` | +| [`tests/tools/`](tools/README.md) | Maintainer commands and CI support scripts | `python3 -m pytest -q tests/tools` | +| [`tests/workflows/`](workflows/README.md) | Exceptional safety properties for repository automation | `python3 -m pytest -q tests/workflows` | The Fortran feature index maps each maintained User Guide and semantic `.pyi` page to its final directory and focused command. The cleanup contract and @@ -28,12 +31,11 @@ progress gates live in ## Ownership contract -Every pytest module and checked fixture has one owner below -`tests/{architecture,fortran,c,shared}/`. `tests/architecture/` contains only -meta-tests of this ownership and evidence system; it is not a compatibility -root for old stage-first tests. No legacy stage-first root, shared fixture -corpus, forwarding fixture, collection shim, import alias, or path fallback is -part of the maintained suite. +Every pytest module and checked fixture has a behavioral owner below +`tests/{docs,tools,workflows,fortran,c}/`. Test organization is a maintainer convention, +not behavior frozen by recursive layout tests. No legacy stage-first root, +shared fixture corpus, forwarding fixture, collection shim, import alias, or +path fallback is part of the maintained suite. ## Final Fortran stage ownership @@ -77,8 +79,9 @@ The final roots must collect and execute independently: ```bash python3 -m pytest -q tests/fortran -m "not real_library" python3 -m pytest -q tests/c -python3 -m pytest -q tests/shared -python3 -m pytest -q tests/architecture +python3 -m pytest -q tests/docs +python3 -m pytest -q tests/tools +python3 -m pytest -q tests/workflows ``` The full-library BLAS/LAPACK integration nodes and the complete correctness @@ -99,7 +102,7 @@ selection: - `property`, `regression`, `benchmark`, and `slow` retain their ordinary meanings; and - `toolchain_smoke` selects only the bounded portable compiler-profile subset - documented in `tests/architecture/fortran/`. + declared by `tests/fortran/conftest.py`. The smoke suite is eight exact nodes reused from ordinary feature end-to-end tests. Strict mode requires a resolved compiler, rejects skips and xfails, and @@ -139,7 +142,24 @@ distinct claim. Unsupported behavior belongs at the first decisive stage. Preserve a later CLI/API diagnostic test only when propagation is itself public behavior. Feature-local fixtures live below their feature; cross-feature helpers require -an explicit infrastructure or shared owner. +an explicit infrastructure owner. + +After choosing feature ownership, mirror genuinely internal mechanisms by +production package and module: + +```text +tests/fortran/infrastructure//test_.py +``` + +Thus `prik/semantics/ownership.py` uses +`infrastructure/semantics/test_ownership.py`, while +`prik/codegen/planner.py` uses `infrastructure/codegen/test_planner.py`. +User-visible behavior does not move to infrastructure merely because it reaches +those modules. Retained production `if __name__ == "__main__"` demonstrations +are smoke-tested by the same dedicated module owner. + +Shared support modules provide builders and assertions only. They do not +re-export `pytest`, standard-library modules, or production symbols. After moving or splitting tests, run collection before execution and compare node IDs, parametrized suffixes, markers, skips, and xfails. Then run every diff --git a/tests/architecture/README.md b/tests/architecture/README.md deleted file mode 100644 index 9f1a2581e..000000000 --- a/tests/architecture/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Test-Suite Architecture Checks - -`tests/architecture/` owns meta-tests whose subject is the test suite itself: -directory ownership, evidence indexes, collection, markers, and bounded -selections. It is outside the language trees so a structural check does not -live inside the structure it validates. - -Language-specific meta-tests use a language subdirectory: - -- `c/` validates the mechanically quarantined C tree and its isolation. -- `fortran/` validates the Fortran feature/stage tree, permanent contract - ledger, real-library isolation, portable smoke selection, and declared - GitHub Actions toolchain lanes. - -Product behavior does not belong here. Fortran behavior stays under -`tests/fortran/`, C behavior stays under `tests/c/`, and genuinely -language-neutral product behavior stays under `tests/shared/`. - -Run this owner independently with: - -```bash -python3 -m pytest -q tests/architecture -``` diff --git a/tests/architecture/c/test_c_language_quarantine.py b/tests/architecture/c/test_c_language_quarantine.py deleted file mode 100644 index 74b9c3f1d..000000000 --- a/tests/architecture/c/test_c_language_quarantine.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Structural contracts for the mechanically quarantined C-input suite.""" - -from __future__ import annotations - -import ast -from pathlib import Path - - -REPO_ROOT = Path(__file__).parents[3] -C_ROOT = REPO_ROOT / "tests/c" -TEST_OWNERS = { - "cli", - "parsing", - "pipeline", - "preprocessing", - "probes", - "semantics", -} -SUPPORT_OWNERS = {"_support", "fixtures"} - - -def _test_imports(path: Path) -> set[str]: - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - imports = {alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names} - imports.update( - node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module is not None - ) - return imports - - -def test_c_tree_has_only_named_test_and_support_owners() -> None: - directories = {path.name for path in C_ROOT.iterdir() if path.is_dir() and path.name != "__pycache__"} - assert directories == TEST_OWNERS | SUPPORT_OWNERS - - for module in C_ROOT.rglob("test_*.py"): - relative = module.relative_to(C_ROOT) - assert relative.parts[0] in TEST_OWNERS - - -def test_c_test_modules_use_only_c_owned_test_support() -> None: - violations = [] - for module in C_ROOT.rglob("test_*.py"): - for imported in _test_imports(module): - if imported == "tests" or not imported.startswith("tests."): - continue - if imported != "tests.c" and not imported.startswith("tests.c."): - violations.append(f"{module.relative_to(REPO_ROOT)}: {imported}") - assert violations == [] - - -def test_c_fixture_and_support_trees_do_not_collect_pytest_modules() -> None: - assert list((C_ROOT / "_support").rglob("test_*.py")) == [] - assert list((C_ROOT / "fixtures").rglob("test_*.py")) == [] diff --git a/tests/architecture/fortran/test_ci_toolchain_lanes.py b/tests/architecture/fortran/test_ci_toolchain_lanes.py deleted file mode 100644 index 5c3977acc..000000000 --- a/tests/architecture/fortran/test_ci_toolchain_lanes.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Validate the declared alternate-compiler GitHub Actions lanes.""" - -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -import pytest - -from tests.fortran._support.wrapper_build import _run_captured_command -from tools.run_fortran_toolchain_lane import FOCUSED_FORTRAN_CLI_NODES, PROFILE_TEST_PATHS, lane_commands - - -REPO_ROOT = Path(__file__).parents[3] -WORKFLOW = REPO_ROOT / ".github/workflows/fortran-toolchain-smoke.yml" -TESTS_WORKFLOW = REPO_ROOT / ".github/workflows/tests.yml" - - -def test_workflow_declares_pinned_ifx_and_flang_pairs_on_ubuntu_2404() -> None: - text = WORKFLOW.read_text(encoding="utf-8") - - assert text.startswith("name: Compiler Compatibility\n") - assert "runs-on: ubuntu-24.04" in text - assert 'PRIK_IFX_VERSION: "2026.1.1"' in text - assert 'PRIK_FLANG_VERSION: "22.1.8"' in text - assert 'PRIK_FLANG_RUNTIME_VERSION: "22.1.7"' in text - for snippet in ( - "- toolchain: ifx", - "display_name: Ubuntu 24.04 · Intel IFX 2026.1.1 · Python 3.12", - "fortran_compiler: ifx", - "c_compiler: icx", - "- toolchain: flang", - "display_name: Ubuntu 24.04 · LLVM Flang 22.1.8 · Python 3.12", - "fortran_compiler: flang", - "c_compiler: clang", - ): - assert snippet in text - - -def test_workflow_installs_pinned_compilers_and_exports_their_runtime() -> None: - text = WORKFLOW.read_text(encoding="utf-8") - - for snippet in ( - "https://software.repos.intel.com/python/conda/", - '"ifx_linux-64=$PRIK_IFX_VERSION"', - '"dpcpp_linux-64=$PRIK_IFX_VERSION"', - '"flang=$PRIK_FLANG_VERSION"', - '"clang=$PRIK_FLANG_VERSION"', - '"libflang-rt=$PRIK_FLANG_RUNTIME_VERSION"', - 'echo "$toolchain_prefix/bin" >> "$GITHUB_PATH"', - 'echo "LD_LIBRARY_PATH=$toolchain_prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV"', - ): - assert snippet in text - - assert 'cache_version: "2026.1.1-v2"' in text - - -def test_every_workflow_lane_delegates_to_the_repository_owned_runner() -> None: - text = WORKFLOW.read_text(encoding="utf-8") - - assert "Show repository-owned lane plan" in text - assert "Run profile checks and strict smoke" in text - assert text.count("python tools/run_fortran_toolchain_lane.py") == 4 - assert '--compiler "$RUNNER_TEMP/${{ matrix.environment }}/bin/${{ matrix.fortran_compiler }}"' in text - - -def test_macos_flang_lane_installs_a_coherent_pair_and_runs_strict_smoke() -> None: - text = WORKFLOW.read_text(encoding="utf-8") - macos_job = text.split(" macos-flang-smoke:\n", maxsplit=1)[1] - - for snippet in ( - "name: Compiler smoke · macOS 15 ARM64 · LLVM Flang · Python 3.12", - "runs-on: macos-15", - "brew install flang", - 'flang_prefix="$(brew --prefix flang)"', - 'llvm_prefix="$(brew --prefix llvm)"', - 'flang_c_include="$flang_prefix/include/flang"', - 'test -f "$flang_c_include/ISO_Fortran_binding.h"', - 'ln -sf "$flang_prefix/bin/flang" "$toolchain_prefix/bin/flang"', - 'ln -sf "$llvm_prefix/bin/clang" "$toolchain_prefix/bin/clang"', - 'echo "C_INCLUDE_PATH=$flang_c_include${C_INCLUDE_PATH:+:$C_INCLUDE_PATH}" >> "$GITHUB_ENV"', - "clang -std=c99 -fsyntax-only -x c -", - "DYLD_LIBRARY_PATH=", - "ImageVersion", - "sw_vers", - "uname -m", - "--compiler flang", - ): - assert snippet in macos_job - - assert macos_job.count("python tools/run_fortran_toolchain_lane.py") == 2 - - -def test_macos_lane_runs_strict_smoke_and_the_full_ordinary_suite() -> None: - text = TESTS_WORKFLOW.read_text(encoding="utf-8") - macos_job = text.split(" macos:\n", maxsplit=1)[1] - - for snippet in ( - "name: Unit tests · macOS 15 ARM64 · Python 3.12", - "runs-on: macos-15", - "PRIK_GFORTRAN_BINARY: gfortran-13", - "PRIK_GCC_BINARY: gcc-13", - 'ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran"', - 'ln -sf "$(command -v "$PRIK_GCC_BINARY")" "$compiler_dir/gcc"', - "ImageVersion", - "sw_vers", - "uname -m", - "python tools/run_fortran_toolchain_lane.py", - "--compiler gfortran", - '-m "not real_library and not toolchain_smoke"', - "tests/architecture", - "tests/c", - "tests/fortran", - "tests/shared", - ): - assert snippet in macos_job - - assert macos_job.count("python tools/run_fortran_toolchain_lane.py") == 2 - - -def test_lane_runner_contains_profile_cli_and_strict_smoke_commands() -> None: - profile, smoke = lane_commands("/opt/toolchain/bin/flang", python_executable="python") - - assert set(PROFILE_TEST_PATHS) < set(profile) - assert set(FOCUSED_FORTRAN_CLI_NODES) < set(profile) - assert smoke[-5:] == ( - "tests/fortran", - "-m", - "toolchain_smoke", - "--require-toolchain-smoke", - "--prik-fortran-compiler=/opt/toolchain/bin/flang", - ) - - -def test_every_profile_and_cli_reference_collects() -> None: - completed = subprocess.run( - [ - sys.executable, - "-m", - "pytest", - "--collect-only", - "-q", - *PROFILE_TEST_PATHS, - *FOCUSED_FORTRAN_CLI_NODES, - ], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=False, - ) - - assert completed.returncode == 0, completed.stdout + completed.stderr - for reference in (*PROFILE_TEST_PATHS, *FOCUSED_FORTRAN_CLI_NODES): - path = reference.partition("::")[0] - assert path in completed.stdout - - -def test_failed_wrapper_build_command_exposes_captured_diagnostics(tmp_path: Path) -> None: - command = [ - sys.executable, - "-c", - ( - "import sys; " - "print('partial build output'); " - "print('flang: actionable compiler error', file=sys.stderr); " - "raise SystemExit(7)" - ), - ] - - with pytest.raises(RuntimeError) as caught: - _run_captured_command(command, cwd=tmp_path) - - message = str(caught.value) - assert "Captured command failed with exit code 7" in message - assert "partial build output" in message - assert "flang: actionable compiler error" in message diff --git a/tests/architecture/fortran/test_contract_coverage_map.py b/tests/architecture/fortran/test_contract_coverage_map.py deleted file mode 100644 index 92fa37e20..000000000 --- a/tests/architecture/fortran/test_contract_coverage_map.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Validate the permanent Fortran contract ledger.""" - -from __future__ import annotations - -import re -import subprocess -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).parents[3] -LEDGER = REPO_ROOT / "tests/fortran/CONTRACT_COVERAGE.md" -CONTRACT_HEADER = ( - "Documentation contract", - "Status", - "Dimensions", - "Stage evidence", - "Runtime evidence", - "Negative evidence", - "CI lane", -) -AUTHORITATIVE_SOURCES = ( - "docs/user/guide/", - "docs/user/reference/pyi-contracts/", - "docs/user/examples/recipes/inspect-fortran-api.md", - "docs/user/examples/recipes/compiler-preprocessing.md", - "docs/user/reference/cli-commands.md", - "docs/user/reference/semantic-ir.md", - "docs/user/reference/semantic-pyi-format.md", - "docs/user/reference/fortran-wrapper.md", - "docs/user/language-support/feature-matrix.md", -) -TERMINAL_STAGES = { - "parsing", - "probes", - "preprocessing", - "semantics", - "policy", - "codegen", - "compiling", - "pipeline", - "import", - "runtime", -} -DOCUMENTATION_LINK = re.compile(r"^\[[^]]+\]\((?P[^)#]+)#(?P[^)]+)\)$") -NODE = re.compile(r"^`(?Ptests/fortran/[^`]+::[^`]+)`$") -NEGATIVE_NODE = re.compile(r"^`(?Ptests/fortran/[^`]+::[^`]+)` \(`(?P[a-z_]+)`\)$") - - -def _split_table_row(line: str) -> tuple[str, ...]: - return tuple(cell.strip() for cell in line.strip().strip("|").split("|")) - - -def _contract_rows() -> list[tuple[str, ...]]: - lines = LEDGER.read_text(encoding="utf-8").splitlines() - header_index = lines.index("| " + " | ".join(CONTRACT_HEADER) + " |") - rows = [] - for line in lines[header_index + 2 :]: - if not line.startswith("|"): - break - row = _split_table_row(line) - assert len(row) == len(CONTRACT_HEADER) - rows.append(row) - return rows - - -def _heading_slug(heading: str) -> str: - heading = re.sub(r"\[([^]]+)\]\([^)]+\)", r"\1", heading) - heading = heading.replace("`", "").replace("*", "").replace("_", "") - heading = re.sub(r"<[^>]+>", "", heading) - heading = re.sub(r"[^\w\s-]", "", heading.casefold()) - return re.sub(r"-+", "-", re.sub(r"\s+", "-", heading)).strip("-") - - -def _document_anchors(path: Path) -> set[str]: - anchors = set() - for line in path.read_text(encoding="utf-8").splitlines(): - match = re.match(r"^#{1,6}\s+(.+?)\s*$", line) - if match: - anchors.add(_heading_slug(match.group(1))) - return anchors - - -def _evidence_nodes(cell: str, *, negative: bool = False) -> list[str]: - if cell == "—": - return [] - pattern = NEGATIVE_NODE if negative else NODE - nodes = [] - for entry in cell.split("
"): - match = pattern.fullmatch(entry.strip()) - assert match is not None, entry - if negative: - assert match.group("stage") in TERMINAL_STAGES - nodes.append(match.group("node")) - return nodes - - -def _collected_nodes(*paths: str) -> set[str]: - result = subprocess.run( - [sys.executable, "-m", "pytest", "--collect-only", "-q", *paths], - cwd=REPO_ROOT, - check=False, - capture_output=True, - text=True, - ) - assert result.returncode == 0, result.stdout + result.stderr - return {line.strip() for line in result.stdout.splitlines() if line.startswith("tests/") and "::" in line} - - -def test_contract_ledger_declares_the_complete_schema_and_sources() -> None: - text = LEDGER.read_text(encoding="utf-8") - assert "| " + " | ".join(CONTRACT_HEADER) + " |" in text - for source in AUTHORITATIVE_SOURCES: - assert source in text - - -def test_contract_documentation_links_resolve_to_stable_headings() -> None: - for row in _contract_rows(): - match = DOCUMENTATION_LINK.fullmatch(row[0]) - assert match is not None, row[0] - path = (LEDGER.parent / match.group("path")).resolve() - assert path.is_file() - assert path.is_relative_to(REPO_ROOT / "docs/user") - assert match.group("anchor") in _document_anchors(path) - - -def test_every_contract_row_retains_evidence_and_blocked_rows_name_the_terminal_stage() -> None: - for row in _contract_rows(): - stage_nodes = _evidence_nodes(row[3]) - runtime_nodes = _evidence_nodes(row[4]) - negative_nodes = _evidence_nodes(row[5], negative=True) - - assert stage_nodes or runtime_nodes or negative_nodes, row[0] - if row[1] == "Blocked": - assert negative_nodes, row[0] - - -def test_contract_evidence_uses_exact_final_nodes_that_collect() -> None: - requested = set() - for row in _contract_rows(): - assert row[1] in {"Supported", "Partially supported", "Blocked"} - requested.update(_evidence_nodes(row[3])) - requested.update(_evidence_nodes(row[4])) - requested.update(_evidence_nodes(row[5], negative=True)) - - if not requested: - return - - paths = sorted({node.split("::", maxsplit=1)[0] for node in requested}) - for relative in paths: - assert (REPO_ROOT / relative).is_file() - assert "/architecture/" not in relative - - collected = _collected_nodes(*paths) - assert requested <= collected diff --git a/tests/architecture/fortran/test_language_ownership.py b/tests/architecture/fortran/test_language_ownership.py deleted file mode 100644 index 982b8f7eb..000000000 --- a/tests/architecture/fortran/test_language_ownership.py +++ /dev/null @@ -1,215 +0,0 @@ -"""Positive ownership and navigation contracts for the Fortran test tree.""" - -from __future__ import annotations - -import ast -import re -from pathlib import Path - - -REPO_ROOT = Path(__file__).parents[3] -TEST_ROOT = REPO_ROOT / "tests" -FORTRAN_ROOT = TEST_ROOT / "fortran" -FEATURE_ROOT = FORTRAN_ROOT -FORTRAN_INDEX = FORTRAN_ROOT / "README.md" -ROADMAP = REPO_ROOT / "docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md" -BLAS_EXAMPLE_ROOT = REPO_ROOT / "examples" / "blas" -LAPACK_EXAMPLE_ROOT = REPO_ROOT / "examples" / "lapack" - -FEATURE_DOCS = { - "data_types": "docs/user/guide/data-types.md", - "arrays": "docs/user/guide/arrays.md", - "strings": "docs/user/guide/strings.md", - "functions": "docs/user/guide/wrapping-functions.md", - "subroutines": "docs/user/guide/wrapping-subroutines.md", - "modules": "docs/user/guide/wrapping-modules.md", - "optional_arguments": "docs/user/guide/optional-arguments.md", - "generic_interfaces": "docs/user/guide/generic-interfaces.md", - "derived_types": "docs/user/guide/wrapping-derived-types.md", - "allocatables": "docs/user/guide/allocatables.md", - "pointers": "docs/user/guide/pointers.md", - "memory_management": "docs/user/guide/memory-management.md", - "callbacks": "docs/user/guide/callbacks.md", - "enumerations": "docs/user/guide/enumerations.md", - "raw_addresses": "docs/user/guide/raw-addresses.md", - "error_handling": "docs/user/guide/error-handling.md", - "building_shared_library": "docs/user/guide/building-shared-library.md", - "source_parsing": "docs/user/examples/recipes/inspect-fortran-api.md", - "source_preprocessing": "docs/user/examples/recipes/compiler-preprocessing.md", - "command_line_interface": "docs/user/reference/cli-commands.md", - "semantic_ir": "docs/user/reference/semantic-ir.md", - "semantic_pyi_format": "docs/user/reference/semantic-pyi-format.md", - "pyi_contracts/exports_and_modules": "docs/user/reference/pyi-contracts/exports-and-modules.md", - "pyi_contracts/functions_and_classes": "docs/user/reference/pyi-contracts/functions-and-classes.md", - "pyi_contracts/calls_and_results": "docs/user/reference/pyi-contracts/calls-and-results.md", -} -STAGES = { - "parsing", - "probes", - "preprocessing", - "semantics", - "policy", - "codegen", - "compiling", - "pipeline", - "runtime", - "end_to_end", -} -FORTRAN_TOP_LEVEL_DIRECTORIES = { - "_support", - *(Path(feature).parts[0] for feature in FEATURE_DOCS), - "infrastructure", -} -INFRASTRUCTURE_OWNERS = { - "infrastructure/policy/", - "infrastructure/codegen/", -} -FEATURE_ROW = re.compile( - r"^\| \[(?P[ x])\] \| \[[^]]+\]\((?P[^)]+)\) " - r"\| `(?P[^`]+)/` \|$", - re.MULTILINE, -) - - -def _roadmap_feature_rows() -> dict[str, tuple[str, Path]]: - rows = {} - matches = list(FEATURE_ROW.finditer(ROADMAP.read_text(encoding="utf-8"))) - assert len(matches) == len(FEATURE_DOCS) - for match in matches: - documentation = (ROADMAP.parent / match.group("documentation")).resolve() - rows[match.group("feature")] = (match.group("status"), documentation) - return rows - - -def _feature_owner(relative: Path) -> str | None: - parts = relative.parts - for feature in sorted(FEATURE_DOCS, key=lambda item: len(Path(item).parts), reverse=True): - owner_parts = Path(feature).parts - if parts[: len(owner_parts)] == owner_parts: - return feature - return None - - -def _python_references(path: Path) -> tuple[set[str], set[str]]: - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - imports = {alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names} - imports.update( - node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module is not None - ) - strings = {node.value for node in ast.walk(tree) if isinstance(node, ast.Constant) and isinstance(node.value, str)} - return imports, strings - - -def test_language_roots_are_documented_and_exist() -> None: - test_index = (TEST_ROOT / "README.md").read_text(encoding="utf-8") - for language in ("fortran", "c", "shared"): - root = TEST_ROOT / language - assert root.is_dir() - assert (root / "README.md").is_file() - assert f"tests/{language}/" in test_index - - -def test_fortran_index_maps_every_documented_feature_to_one_command() -> None: - text = FORTRAN_INDEX.read_text(encoding="utf-8") - for feature, documentation in FEATURE_DOCS.items(): - assert (REPO_ROOT / documentation).is_file() - assert text.count(f"](../../{documentation})") == 1 - assert text.count(f"`{feature}/`") == 1 - assert text.count(f"`python3 -m pytest -q tests/fortran/{feature}`") == 1 - - -def test_roadmap_feature_rows_match_the_fortran_index_contract() -> None: - rows = _roadmap_feature_rows() - assert set(rows) == set(FEATURE_DOCS) - for feature, (status, documentation) in rows.items(): - assert documentation == (REPO_ROOT / FEATURE_DOCS[feature]).resolve() - if status == "x": - assert (FEATURE_ROOT / feature).is_dir() - - -def test_fortran_tree_uses_only_documented_top_level_owners() -> None: - actual = {path.name for path in FORTRAN_ROOT.iterdir() if path.is_dir() and path.name != "__pycache__"} - assert actual <= FORTRAN_TOP_LEVEL_DIRECTORIES - - -def test_feature_directories_and_pytest_modules_have_documented_owners() -> None: - if not FEATURE_ROOT.exists(): - return - - allowed_roots = {Path(feature).parts[0] for feature in FEATURE_DOCS} - actual_roots = { - path.name - for path in FEATURE_ROOT.iterdir() - if path.is_dir() and path.name not in {"__pycache__", "_support", "infrastructure"} - } - assert actual_roots <= allowed_roots - - pyi_contracts = FEATURE_ROOT / "pyi_contracts" - if pyi_contracts.exists(): - allowed_pyi_owners = { - Path(feature).parts[1] for feature in FEATURE_DOCS if feature.startswith("pyi_contracts/") - } - actual_pyi_owners = { - path.name for path in pyi_contracts.iterdir() if path.is_dir() and path.name != "__pycache__" - } - assert actual_pyi_owners <= allowed_pyi_owners - - for feature in FEATURE_DOCS: - owner = FEATURE_ROOT / feature - if not owner.exists(): - continue - child_directories = {path.name for path in owner.iterdir() if path.is_dir() and path.name != "__pycache__"} - assert child_directories <= STAGES - - feature_modules = ( - module - for module in FEATURE_ROOT.rglob("test_*.py") - if module.relative_to(FEATURE_ROOT).parts[0] in allowed_roots - ) - for module in feature_modules: - relative = module.relative_to(FEATURE_ROOT) - owner = _feature_owner(relative) - assert owner is not None, relative.as_posix() - owner_depth = len(Path(owner).parts) - assert len(relative.parts) > owner_depth + 1, relative.as_posix() - assert relative.parts[owner_depth] in STAGES, relative.as_posix() - - -def test_fortran_index_names_every_infrastructure_owner() -> None: - text = FORTRAN_INDEX.read_text(encoding="utf-8") - for owner in INFRASTRUCTURE_OWNERS: - assert f"`{owner}`" in text - - -def test_real_library_examples_have_single_native_source_owners() -> None: - assert (BLAS_EXAMPLE_ROOT / "native").is_dir() - assert (LAPACK_EXAMPLE_ROOT / "native").is_dir() - assert len(tuple((BLAS_EXAMPLE_ROOT / "native").iterdir())) == 155 - assert len(tuple((LAPACK_EXAMPLE_ROOT / "native").iterdir())) == 2062 - - -def test_final_language_roots_do_not_cross_import_or_read_fixtures() -> None: - forbidden_by_root = { - TEST_ROOT / "fortran": (("tests.c", "tests/c/"),), - TEST_ROOT / "c": (("tests.fortran", "tests/fortran/"),), - TEST_ROOT / "shared": ( - ("tests.c", "tests/c/"), - ("tests.fortran", "tests/fortran/"), - ), - } - violations = [] - for root, forbidden in forbidden_by_root.items(): - for path in root.rglob("*.py"): - relative = path.relative_to(root) - if "architecture" in relative.parts: - continue - imports, strings = _python_references(path) - for reference in imports: - if any(reference == module or reference.startswith(f"{module}.") for module, _ in forbidden): - violations.append(f"{path.relative_to(REPO_ROOT)}: {reference}") - if root == TEST_ROOT / "shared" and relative.parts[0] == "docs": - continue - for reference in strings: - if any(fixture_path in reference for _, fixture_path in forbidden): - violations.append(f"{path.relative_to(REPO_ROOT)}: {reference}") - assert violations == [] diff --git a/tests/architecture/fortran/test_smoke_selection.py b/tests/architecture/fortran/test_smoke_selection.py deleted file mode 100644 index e49cc73e2..000000000 --- a/tests/architecture/fortran/test_smoke_selection.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Validate the exact portable toolchain smoke selection.""" - -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -from tests.fortran.conftest import TOOLCHAIN_SMOKE_CASES - -REPO_ROOT = Path(__file__).parents[3] -FEATURE_ROOT = REPO_ROOT / "tests/fortran" -CONTRACT_LEDGER = REPO_ROOT / "tests/fortran/CONTRACT_COVERAGE.md" -PYPROJECT = REPO_ROOT / "pyproject.toml" -REQUIRED_MECHANISMS = { - "allocatable_ownership", - "callback_boundary", - "derived_type_lifecycle", - "generic_overload_dispatch", - "numpy_array_layout", - "scalar_module_procedure", - "source_generated_pyi_rebuild", - "string_copy_in_out", -} -PROHIBITED_PATH_PARTS = { - "platform", - "platforms", - "profile", - "profiles", -} - - -def _collect(*args: str) -> tuple[set[str], str]: - result = subprocess.run( - [sys.executable, "-m", "pytest", "--collect-only", "-q", *args], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=False, - ) - assert result.returncode == 0, result.stdout + result.stderr - nodes = { - line.strip() - for line in result.stdout.splitlines() - if line.startswith("tests/fortran/") and "::" in line and " | mechanism=" not in line - } - return nodes, result.stdout - - -def test_toolchain_smoke_marker_is_registered_with_the_exact_contract() -> None: - expected = ( - '"toolchain_smoke: portable compiled Fortran end-to-end cases reused across compiler, OS, ' - 'and architecture lanes"' - ) - assert expected in PYPROJECT.read_text(encoding="utf-8") - - -def test_toolchain_smoke_manifest_has_the_required_mechanisms_and_build_budget() -> None: - mechanisms = {case.mechanism for case in TOOLCHAIN_SMOKE_CASES.values()} - build_fixtures = {case.build_fixture for case in TOOLCHAIN_SMOKE_CASES.values()} - - assert mechanisms == REQUIRED_MECHANISMS - assert len(mechanisms) == len(TOOLCHAIN_SMOKE_CASES) - assert 6 <= len(build_fixtures) <= 8 - assert len(build_fixtures) == len(TOOLCHAIN_SMOKE_CASES) - - -def test_toolchain_smoke_manifest_uses_only_feature_end_to_end_nodes() -> None: - for nodeid in TOOLCHAIN_SMOKE_CASES: - path_text, separator, test_name = nodeid.partition("::") - path = Path(path_text) - assert separator and test_name - assert path.parts[:2] == ("tests", "fortran") - assert path.parts[2] not in {"_support", "infrastructure"} - assert "end_to_end" in path.parts - assert PROHIBITED_PATH_PARTS.isdisjoint(path.parts) - assert (REPO_ROOT / path).is_file() - - -def test_every_toolchain_smoke_node_is_permanent_contract_evidence() -> None: - ledger = CONTRACT_LEDGER.read_text(encoding="utf-8") - for nodeid in TOOLCHAIN_SMOKE_CASES: - assert f"`{nodeid}`" in ledger - - -def test_toolchain_smoke_collects_exactly_the_manifest_and_reports_it_in_sorted_order() -> None: - nodes, output = _collect( - str(FEATURE_ROOT.relative_to(REPO_ROOT)), - "-m", - "toolchain_smoke", - "--require-toolchain-smoke", - f"--prik-fortran-compiler={sys.executable}", - ) - assert nodes == set(TOOLCHAIN_SMOKE_CASES) - - reported_nodes = [ - line.split(" | mechanism=", maxsplit=1)[0] - for line in output.splitlines() - if line.startswith("tests/fortran/") and " | mechanism=" in line - ] - assert reported_nodes == sorted(TOOLCHAIN_SMOKE_CASES) - - -def test_toolchain_smoke_nodes_are_in_the_ordinary_end_to_end_suite() -> None: - ordinary_nodes, _output = _collect( - str(FEATURE_ROOT.relative_to(REPO_ROOT)), - "-m", - "fortran_end_to_end", - ) - assert set(TOOLCHAIN_SMOKE_CASES) <= ordinary_nodes - - -def test_explicit_missing_toolchain_smoke_compiler_is_an_error() -> None: - missing = REPO_ROOT / ".missing-prik-fortran-compiler" - result = subprocess.run( - [ - sys.executable, - "-m", - "pytest", - "--collect-only", - "-q", - str(FEATURE_ROOT.relative_to(REPO_ROOT)), - "-m", - "toolchain_smoke", - "--require-toolchain-smoke", - f"--prik-fortran-compiler={missing}", - ], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=False, - ) - assert result.returncode == pytest_usage_error_exit_code() - assert f"requested Fortran compiler is unavailable: {missing}" in result.stderr - - -def pytest_usage_error_exit_code() -> int: - """Keep the subprocess assertion readable without importing pytest internals.""" - - return 4 diff --git a/tests/c/README.md b/tests/c/README.md index a5d236b28..6f150a380 100644 --- a/tests/c/README.md +++ b/tests/c/README.md @@ -4,9 +4,6 @@ CPython binding code used to implement a Fortran wrapper remains under the owning Fortran feature. -The meta-test that validates this directory's quarantine and isolation lives -outside it under `tests/architecture/c/`. - C receives a mechanical quarantine during the language-first migration. Move existing C parsing, probes, preprocessing, semantic conversion, pipeline, CLI dispatch, property tests, fixtures, and helpers without redesigning their diff --git a/tests/c/_support/cli.py b/tests/c/_support/cli.py index c6096e7d9..1eb2bf5cf 100644 --- a/tests/c/_support/cli.py +++ b/tests/c/_support/cli.py @@ -82,6 +82,3 @@ def error(self, message): parser = FakeParser() monkeypatch.setattr(prik_cli, "_parser_for_argv", lambda argv: (parser, argv)) return parser - - -__all__ = ("_install_main_parser", "_main_args", "prik_cli", "types") diff --git a/tests/c/_support/parser_properties.py b/tests/c/_support/parser_properties.py index baa332734..c8beb2882 100644 --- a/tests/c/_support/parser_properties.py +++ b/tests/c/_support/parser_properties.py @@ -2,18 +2,12 @@ from __future__ import annotations -import json -from contextlib import suppress - import pytest pytest.importorskip("hypothesis") -from hypothesis import given, strategies as st - -from prik.parsers.c import CParseError, parse_c_file -from prik.parsers.c.lexer import split_top_level_c_source, top_level_split +from hypothesis import strategies as st _C_SCALAR_TYPES = st.sampled_from(["int", "double", "float", "char"]) @@ -75,20 +69,3 @@ def c_nested_variable_declarations(draw): components = ["CPointer", "CFunctionType"] return source, components - - -__all__ = ( - "_C_IDENTIFIERS", - "_FUZZ_TEXT", - "CParseError", - "c_nested_variable_declarations", - "c_prototypes", - "given", - "json", - "parse_c_file", - "pytest", - "split_top_level_c_source", - "st", - "suppress", - "top_level_split", -) diff --git a/tests/c/_support/preprocessing.py b/tests/c/_support/preprocessing.py index b425f3afe..b7e6ba480 100644 --- a/tests/c/_support/preprocessing.py +++ b/tests/c/_support/preprocessing.py @@ -2,25 +2,13 @@ from __future__ import annotations -import json import os -import subprocess import sys from pathlib import Path import pytest -import prik.pipeline.preprocessing as preprocessing -from prik.pipeline.preprocessing import ( - PreprocessingConfig, - PreprocessingError, - build_compile_commands_invocation, - build_direct_preprocess_invocation, - build_preprocess_invocation, - build_template_preprocess_invocation, - run_compiler_preprocessor, - run_compiler_preprocessor_with_recipe, -) +from prik.pipeline.preprocessing import PreprocessingError def _fake_compiler(tmp_path: Path, output: str) -> tuple[Path, Path, dict[str, str]]: @@ -70,24 +58,3 @@ def _assert_preprocessing_error( assert str(exc_info.value) == message assert exc_info.value.category == category assert exc_info.value.diagnostics == [] - - -__all__ = ( - "Path", - "PreprocessingConfig", - "PreprocessingError", - "_assert_preprocessing_error", - "_failing_compiler", - "_fake_compiler", - "build_compile_commands_invocation", - "build_direct_preprocess_invocation", - "build_preprocess_invocation", - "build_template_preprocess_invocation", - "json", - "preprocessing", - "pytest", - "run_compiler_preprocessor", - "run_compiler_preprocessor_with_recipe", - "subprocess", - "sys", -) diff --git a/tests/c/_support/semantic_properties.py b/tests/c/_support/semantic_properties.py index 4cb669ed3..dcdab426c 100644 --- a/tests/c/_support/semantic_properties.py +++ b/tests/c/_support/semantic_properties.py @@ -2,17 +2,12 @@ from __future__ import annotations -from dataclasses import asdict - import pytest pytest.importorskip("hypothesis") -from hypothesis import given, strategies as st - -from prik.parsers.c import parse_c_file -from prik.semantics.c2ir import c_file_to_semantic_modules +from hypothesis import strategies as st _C_SCALAR_TYPES = st.sampled_from( @@ -48,14 +43,3 @@ def c_scalar_prototypes(draw): for parameter_id, (_source_type, semantic_type) in zip(parameter_ids, parameter_types, strict=True) ] return source, semantic_result, expected_parameters - - -__all__ = ( - "_C_VALUE_TYPES", - "asdict", - "c_file_to_semantic_modules", - "c_scalar_prototypes", - "given", - "parse_c_file", - "pytest", -) diff --git a/tests/c/cli/test_c_cli_argument_contract.py b/tests/c/cli/test_c_cli_argument_contract.py index ef528d389..875df1364 100644 --- a/tests/c/cli/test_c_cli_argument_contract.py +++ b/tests/c/cli/test_c_cli_argument_contract.py @@ -1,15 +1,15 @@ """C input-language CLI argument contracts.""" from pathlib import Path +import types import pytest +from prik import cli as prik_cli from tests.c._support.cli import ( _MainParserError, _install_main_parser, _main_args, - types, - prik_cli, ) diff --git a/tests/c/cli/test_c_cli_output_contract.py b/tests/c/cli/test_c_cli_output_contract.py index cece9965e..e15d44a02 100644 --- a/tests/c/cli/test_c_cli_output_contract.py +++ b/tests/c/cli/test_c_cli_output_contract.py @@ -1,10 +1,11 @@ """C input-language CLI output contracts.""" +import types + +from prik import cli as prik_cli from tests.c._support.cli import ( _install_main_parser, _main_args, - types, - prik_cli, ) diff --git a/tests/c/cli/test_c_cli_stage_dispatch.py b/tests/c/cli/test_c_cli_stage_dispatch.py index 7645e3a4c..62db51b34 100644 --- a/tests/c/cli/test_c_cli_stage_dispatch.py +++ b/tests/c/cli/test_c_cli_stage_dispatch.py @@ -4,10 +4,10 @@ import pytest +from prik import cli as prik_cli from tests.c._support.cli import ( _install_main_parser, _main_args, - prik_cli, ) diff --git a/tests/c/parsing/test_c_parser_properties.py b/tests/c/parsing/test_c_parser_properties.py index 58d1a2d3f..0cc4ec8eb 100644 --- a/tests/c/parsing/test_c_parser_properties.py +++ b/tests/c/parsing/test_c_parser_properties.py @@ -1,19 +1,18 @@ """Tests split by stable ownership concept from `test_properties.py`.""" +from contextlib import suppress +import json + +import pytest + +from hypothesis import given, strategies as st +from prik.parsers.c import CParseError, parse_c_file +from prik.parsers.c.lexer import split_top_level_c_source, top_level_split from tests.c._support.parser_properties import ( - CParseError, _C_IDENTIFIERS, _FUZZ_TEXT, c_nested_variable_declarations, c_prototypes, - given, - json, - parse_c_file, - pytest, - split_top_level_c_source, - st, - suppress, - top_level_split, ) diff --git a/tests/c/preprocessing/test_c_preprocessing_cli.py b/tests/c/preprocessing/test_c_preprocessing_cli.py index 386f19ff6..6707ca74d 100644 --- a/tests/c/preprocessing/test_c_preprocessing_cli.py +++ b/tests/c/preprocessing/test_c_preprocessing_cli.py @@ -1,12 +1,13 @@ """Tests split by stable ownership concept from `test_cli.py`.""" +import json +from pathlib import Path +import subprocess +import sys + from tests.c._support.preprocessing import ( - Path, _failing_compiler, _fake_compiler, - json, - subprocess, - sys, ) diff --git a/tests/c/preprocessing/test_c_preprocessing_configuration.py b/tests/c/preprocessing/test_c_preprocessing_configuration.py index d41004142..a7194c6d5 100644 --- a/tests/c/preprocessing/test_c_preprocessing_configuration.py +++ b/tests/c/preprocessing/test_c_preprocessing_configuration.py @@ -1,18 +1,20 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.c._support.preprocessing import ( - Path, +import json +from pathlib import Path + +import pytest + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingError, - _assert_preprocessing_error, build_compile_commands_invocation, build_direct_preprocess_invocation, build_preprocess_invocation, build_template_preprocess_invocation, - json, - preprocessing, - pytest, ) +from tests.c._support.preprocessing import _assert_preprocessing_error def test_direct_c_preprocess_invocation_uses_exact_compiler_and_flags(tmp_path: Path): diff --git a/tests/c/preprocessing/test_c_preprocessing_dependencies.py b/tests/c/preprocessing/test_c_preprocessing_dependencies.py index fdf60a378..f6e08d0a1 100644 --- a/tests/c/preprocessing/test_c_preprocessing_dependencies.py +++ b/tests/c/preprocessing/test_c_preprocessing_dependencies.py @@ -1,12 +1,10 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.c._support.preprocessing import ( - Path, - PreprocessingConfig, - build_compile_commands_invocation, - json, - preprocessing, -) +import json +from pathlib import Path + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import PreprocessingConfig, build_compile_commands_invocation def test_compile_commands_filters_dependency_and_windows_compile_flags(tmp_path: Path): diff --git a/tests/c/preprocessing/test_c_preprocessing_execution.py b/tests/c/preprocessing/test_c_preprocessing_execution.py index a21fb6f27..f7e72b5d7 100644 --- a/tests/c/preprocessing/test_c_preprocessing_execution.py +++ b/tests/c/preprocessing/test_c_preprocessing_execution.py @@ -1,16 +1,18 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.c._support.preprocessing import ( - Path, +import json +from pathlib import Path +import subprocess +import sys + +import pytest + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingError, - json, - preprocessing, - pytest, run_compiler_preprocessor, run_compiler_preprocessor_with_recipe, - subprocess, - sys, ) diff --git a/tests/c/preprocessing/test_c_preprocessing_properties.py b/tests/c/preprocessing/test_c_preprocessing_properties.py index fbc1ceee7..018aae0bb 100644 --- a/tests/c/preprocessing/test_c_preprocessing_properties.py +++ b/tests/c/preprocessing/test_c_preprocessing_properties.py @@ -1,12 +1,11 @@ """Property-based preprocessing invariants for generated C sources.""" +import pytest + +from hypothesis import given, strategies as st +from prik.parsers.c import CParseError, parse_c_file from tests.c._support.parser_properties import ( - CParseError, _C_IDENTIFIERS, - given, - parse_c_file, - pytest, - st, ) diff --git a/tests/c/preprocessing/test_error_paths.py b/tests/c/preprocessing/test_error_paths.py index 75b5a9fd4..51311f089 100644 --- a/tests/c/preprocessing/test_error_paths.py +++ b/tests/c/preprocessing/test_error_paths.py @@ -1,13 +1,15 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.c._support.preprocessing import ( - Path, +from pathlib import Path +import subprocess +import sys + +import pytest + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingError, - preprocessing, - pytest, - subprocess, - sys, ) diff --git a/tests/c/preprocessing/test_source_mappings.py b/tests/c/preprocessing/test_source_mappings.py index 8df090d2f..c82e04e07 100644 --- a/tests/c/preprocessing/test_source_mappings.py +++ b/tests/c/preprocessing/test_source_mappings.py @@ -1,10 +1,9 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.c._support.preprocessing import ( - Path, - PreprocessingConfig, - preprocessing, -) +from pathlib import Path + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import PreprocessingConfig def test_preprocess_source_preserves_plain_c_source_mapping(monkeypatch, tmp_path: Path): diff --git a/tests/c/semantics/conversion/_support.py b/tests/c/semantics/conversion/_support.py index ea4654658..c9faa4436 100644 --- a/tests/c/semantics/conversion/_support.py +++ b/tests/c/semantics/conversion/_support.py @@ -2,85 +2,6 @@ from dataclasses import asdict -from typing import ClassVar - -import pytest - -from prik.parsers.c import parse_c_file, parse_c_project - -from prik.parsers.c.models import ( - CArray, - CAtomic, - CBool, - CChar, - CComposedType, - CConst, - CDiagnostic, - CDouble, - CDoubleComplex, - CEnum, - CFile, - CFloat, - CFloatComplex, - CFunction, - CFunctionType, - CInitializer, - CInt, - CLong, - CLongDouble, - CLongDoubleComplex, - CLongLong, - CMacro, - CMacroDependency, - CParameter, - CPointer, - CProject, - CRestrict, - CShort, - CSignedChar, - CSourceLocation, - CStruct, - CTypedef, - CUnion, - CUnknownType, - CUnsignedChar, - CUnsignedInt, - CUnsignedLong, - CUnsignedLongLong, - CUnsignedShort, - CVariable, - CVolatile, - CVoid, -) - -from prik.semantics.c2ir import ( - CToIRConverter, - c_file_to_semantic_module, - c_file_to_semantic_modules, - c_function_to_semantic_function, - c_parameter_to_semantic_argument, - c_project_to_semantic_module, - c_project_to_semantic_modules, - c_struct_to_semantic_class, - c_type_to_semantic_type, -) - -from prik.semantics.models import ( - SemanticArgument, - SemanticClass, - SemanticField, - SemanticModule, - SemanticOrigin, - SemanticStorageContract, - SemanticType, - SemanticVariable, -) - -from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text - - -from prik.codegen.printers import emit_module, emit_module_stubs - def _function(module, name): return next(function for function in module.functions if function.name == name) @@ -119,77 +40,3 @@ def _assert_unsupported_type(semantic_type, *, code, message, owner, source_type source_kind="unsupported_type", source_type=source_type, ) - - -__all__ = ( - "CArray", - "CAtomic", - "CBool", - "CChar", - "CComposedType", - "CConst", - "CDiagnostic", - "CDouble", - "CDoubleComplex", - "CEnum", - "CFile", - "CFloat", - "CFloatComplex", - "CFunction", - "CFunctionType", - "CInitializer", - "CInt", - "CLong", - "CLongDouble", - "CLongDoubleComplex", - "CLongLong", - "CMacro", - "CMacroDependency", - "CParameter", - "CPointer", - "CProject", - "CRestrict", - "CShort", - "CSignedChar", - "CSourceLocation", - "CStruct", - "CToIRConverter", - "CTypedef", - "CUnion", - "CUnknownType", - "CUnsignedChar", - "CUnsignedInt", - "CUnsignedLong", - "CUnsignedLongLong", - "CUnsignedShort", - "CVariable", - "CVoid", - "CVolatile", - "ClassVar", - "SemanticArgument", - "SemanticClass", - "SemanticField", - "SemanticModule", - "SemanticOrigin", - "SemanticStorageContract", - "SemanticType", - "SemanticVariable", - "_assert_unsupported_type", - "_c_origin", - "_function", - "asdict", - "c_file_to_semantic_module", - "c_file_to_semantic_modules", - "c_function_to_semantic_function", - "c_parameter_to_semantic_argument", - "c_project_to_semantic_module", - "c_project_to_semantic_modules", - "c_struct_to_semantic_class", - "c_type_to_semantic_type", - "emit_module", - "emit_module_stubs", - "parse_c_file", - "parse_c_project", - "parse_pyi_text", - "pytest", -) diff --git a/tests/c/semantics/conversion/test_c_conversion_properties.py b/tests/c/semantics/conversion/test_c_conversion_properties.py index 8040a0630..6be2a2c3f 100644 --- a/tests/c/semantics/conversion/test_c_conversion_properties.py +++ b/tests/c/semantics/conversion/test_c_conversion_properties.py @@ -1,13 +1,15 @@ """Tests split by stable ownership concept from `test_c_conversion_properties.py`.""" +from dataclasses import asdict + +import pytest + +from hypothesis import given +from prik.parsers.c import parse_c_file +from prik.semantics.c2ir import c_file_to_semantic_modules from tests.c._support.semantic_properties import ( - asdict, _C_VALUE_TYPES, - c_file_to_semantic_modules, c_scalar_prototypes, - given, - parse_c_file, - pytest, ) diff --git a/tests/c/semantics/conversion/test_functions_and_callbacks.py b/tests/c/semantics/conversion/test_functions_and_callbacks.py index 0197a27da..357f1e2cf 100644 --- a/tests/c/semantics/conversion/test_functions_and_callbacks.py +++ b/tests/c/semantics/conversion/test_functions_and_callbacks.py @@ -1,6 +1,9 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" -from tests.c.semantics.conversion._support import ( +from dataclasses import asdict + +from prik.parsers.c import parse_c_file +from prik.parsers.c.models import ( CAtomic, CChar, CComposedType, @@ -12,18 +15,16 @@ CPointer, CSourceLocation, CStruct, - CToIRConverter, CTypedef, CUnknownType, CVariable, - CVoid, CVolatile, + CVoid, +) +from prik.semantics.c2ir import CToIRConverter, c_file_to_semantic_modules, c_function_to_semantic_function +from tests.c.semantics.conversion._support import ( _c_origin, _function, - asdict, - c_file_to_semantic_modules, - c_function_to_semantic_function, - parse_c_file, ) diff --git a/tests/c/semantics/conversion/test_projects_and_diagnostics.py b/tests/c/semantics/conversion/test_projects_and_diagnostics.py index c89771d46..a9edcd190 100644 --- a/tests/c/semantics/conversion/test_projects_and_diagnostics.py +++ b/tests/c/semantics/conversion/test_projects_and_diagnostics.py @@ -1,10 +1,15 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" +from dataclasses import asdict from pathlib import Path import subprocess import sys -from tests.c.semantics.conversion._support import ( +import pytest + +from prik.codegen.printers import emit_module_stubs +from prik.parsers.c import parse_c_file, parse_c_project +from prik.parsers.c.models import ( CEnum, CFile, CFunction, @@ -13,27 +18,23 @@ CProject, CSourceLocation, CStruct, - CToIRConverter, CTypedef, CUnion, CVariable, - SemanticArgument, - SemanticModule, - SemanticOrigin, - SemanticType, - _c_origin, - _function, - asdict, +) +from prik.semantics.c2ir import ( + CToIRConverter, c_file_to_semantic_module, c_parameter_to_semantic_argument, c_project_to_semantic_module, c_project_to_semantic_modules, c_struct_to_semantic_class, c_type_to_semantic_type, - emit_module_stubs, - parse_c_file, - parse_c_project, - pytest, +) +from prik.semantics.models import SemanticArgument, SemanticModule, SemanticOrigin, SemanticType +from tests.c.semantics.conversion._support import ( + _c_origin, + _function, ) diff --git a/tests/c/semantics/conversion/test_records_and_enums.py b/tests/c/semantics/conversion/test_records_and_enums.py index d4732b160..058efe8df 100644 --- a/tests/c/semantics/conversion/test_records_and_enums.py +++ b/tests/c/semantics/conversion/test_records_and_enums.py @@ -1,6 +1,10 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" -from tests.c.semantics.conversion._support import ( +from dataclasses import asdict + +from prik.codegen.printers import emit_module, emit_module_stubs +from prik.parsers.c import parse_c_file, parse_c_project +from prik.parsers.c.models import ( CArray, CComposedType, CDouble, @@ -13,11 +17,20 @@ CPointer, CSourceLocation, CStruct, - CToIRConverter, CTypedef, CUnion, CUnknownType, CVariable, +) +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text +from prik.semantics.c2ir import ( + CToIRConverter, + c_file_to_semantic_module, + c_file_to_semantic_modules, + c_project_to_semantic_module, + c_project_to_semantic_modules, +) +from prik.semantics.models import ( SemanticArgument, SemanticClass, SemanticField, @@ -25,18 +38,10 @@ SemanticOrigin, SemanticType, SemanticVariable, +) +from tests.c.semantics.conversion._support import ( _c_origin, _function, - asdict, - c_file_to_semantic_module, - c_file_to_semantic_modules, - c_project_to_semantic_module, - c_project_to_semantic_modules, - emit_module, - emit_module_stubs, - parse_c_file, - parse_c_project, - parse_pyi_text, ) diff --git a/tests/c/semantics/conversion/test_types_and_constants.py b/tests/c/semantics/conversion/test_types_and_constants.py index 9ea5892ec..5a450c96a 100644 --- a/tests/c/semantics/conversion/test_types_and_constants.py +++ b/tests/c/semantics/conversion/test_types_and_constants.py @@ -1,8 +1,12 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" +from dataclasses import asdict from typing import ClassVar -from tests.c.semantics.conversion._support import ( +import pytest + +from prik.parsers.c import parse_c_file +from prik.parsers.c.models import ( CArray, CBool, CChar, @@ -27,7 +31,6 @@ CSignedChar, CSourceLocation, CStruct, - CToIRConverter, CTypedef, CUnknownType, CUnsignedChar, @@ -36,10 +39,9 @@ CUnsignedLongLong, CUnsignedShort, CVariable, - _assert_unsupported_type, - _c_origin, - _function, - asdict, +) +from prik.semantics.c2ir import ( + CToIRConverter, c_file_to_semantic_module, c_file_to_semantic_modules, c_function_to_semantic_function, @@ -48,8 +50,11 @@ c_project_to_semantic_modules, c_struct_to_semantic_class, c_type_to_semantic_type, - parse_c_file, - pytest, +) +from tests.c.semantics.conversion._support import ( + _assert_unsupported_type, + _c_origin, + _function, ) diff --git a/tests/docs/README.md b/tests/docs/README.md new file mode 100644 index 000000000..110dab7b4 --- /dev/null +++ b/tests/docs/README.md @@ -0,0 +1,15 @@ +# Documentation Tests + +This top-level feature owns repository documentation metadata, visibility, +navigation, executable examples, publication, user-content journeys, and +public reference/source-map synchronization. + +Each pytest module owns one documentation invariant family. Shared parsing +facts live in `_structure_support.py`; unrelated repository architecture, +workflow, and product behavior tests do not belong here. + +Run this owner independently with: + +```bash +python3 -m pytest -q tests/docs +``` diff --git a/tests/docs/_structure_support.py b/tests/docs/_structure_support.py new file mode 100644 index 000000000..009391193 --- /dev/null +++ b/tests/docs/_structure_support.py @@ -0,0 +1,629 @@ +"""Shared facts and parsers for documentation contract tests.""" + +from __future__ import annotations + +from functools import cache +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).parents[2] +DOCS_ROOT = ROOT / "docs" +FEATURE_MATRIX_PATH = DOCS_ROOT / "user/language-support/feature-matrix.md" +CLI_REFERENCE_PATH = DOCS_ROOT / "user/reference/cli-commands.md" +PYTHON_API_REFERENCE_PATH = DOCS_ROOT / "user/reference/python-api.md" +DOCUMENTATION_CHECKLIST_PATH = DOCS_ROOT / "maintainer/roadmap/documentation-content-checklist.md" +DOC_PATHS = sorted(path for path in DOCS_ROOT.rglob("*.md") if "old_docs" not in path.parts) +WEBSITE_DOCUMENTATION_PATHS = [ + DOCS_ROOT / "index.md", + *sorted((DOCS_ROOT / "user").rglob("*.md")), + *sorted((DOCS_ROOT / "developer").rglob("*.md")), + *sorted((DOCS_ROOT / "maintainer").rglob("*.md")), +] +LEARNING_DOCUMENTATION_PATHS = [ + *sorted((DOCS_ROOT / "user").rglob("*.md")), + *sorted((DOCS_ROOT / "developer").rglob("*.md")), +] +DEFERRED_C_PAGE_PATHS = [ + ROOT / "docs/maintainer/design/cpython-integration.md", + ROOT / "docs/developer/c-parser-reference.md", + ROOT / "docs/user/examples/recipes/inspect-c-api.md", +] +MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)#]+)(?:#[^)]+)?\)") +NEXT_NAVIGATION = re.compile(r"^\s*(?:#{2,6}\s+Next|\*\*Next\*\*:?)\s*$", re.IGNORECASE) +NEXT_SECTION_BOUNDARY = re.compile(r"^\s*(?:#{2,6}\s+|---\s*$|\*\*[^*]+\*\*)") +ALLOWED_CONTEXTUAL_FORWARD_LINK_SOURCE_PREFIXES = ("user/getting-started/", "user/guide/") +ALLOWED_CONTEXTUAL_FORWARD_LINK_PREFIXES = ("user/reference/pyi-contracts/",) +C_DOCS_START = "" +C_DOCS_DISABLED = "" and hidden == "ordinary": + hidden = None + elif hidden == "deferred-c": + assert "--" not in line, f"{path.relative_to(ROOT)}: invalid double hyphen in deferred comment" + elif hidden == "ordinary": + continue + elif not line.lstrip().startswith(C_DOCS_DISABLED): + visible.append(line) + assert not hidden, f"{path.relative_to(ROOT)}: unclosed deferred documentation comment" + return "\n".join(visible) + + +def _instructional_body_without_next(body: str) -> str: + instructional_lines: list[str] = [] + inside_next = False + + for line in body.splitlines(): + if NEXT_NAVIGATION.match(line): + inside_next = True + continue + if inside_next and NEXT_SECTION_BOUNDARY.match(line): + inside_next = False + if not inside_next: + instructional_lines.append(line) + + return "\n".join(instructional_lines) + + +def _next_navigation_items(body: str) -> list[tuple[int, str, bool]]: + items: list[tuple[int, str, bool]] = [] + current_line: int | None = None + current_parts: list[str] = [] + inside_next = False + + def flush_current() -> None: + nonlocal current_line, current_parts + if current_line is not None: + items.append((current_line, " ".join(current_parts), True)) + current_line = None + current_parts = [] + + for line_number, line in enumerate(body.splitlines(), start=1): + if NEXT_NAVIGATION.match(line): + flush_current() + inside_next = True + continue + if inside_next and NEXT_SECTION_BOUNDARY.match(line): + flush_current() + inside_next = False + continue + if not inside_next or not line.strip(): + continue + if line.startswith("- "): + flush_current() + current_line = line_number + current_parts = [line[2:].strip()] + elif current_line is not None and line.startswith(" "): + current_parts.append(line.strip()) + else: + flush_current() + items.append((line_number, line.strip(), False)) + + flush_current() + return items + + +def _combined_text(relative_paths: list[str]) -> str: + return "\n".join((ROOT / relative_path).read_text(encoding="utf-8") for relative_path in relative_paths) + + +@cache +def _site_navigation_positions() -> dict[str, int]: + navigation_entry = re.compile(r": ([^#\s]+\.md)\s*$") + paths: list[str] = [] + for line in (ROOT / "mkdocs.yml").read_text(encoding="utf-8").splitlines(): + if line.lstrip().startswith("#"): + continue + match = navigation_entry.search(line) + if match: + paths.append(match.group(1)) + return {path: index for index, path in enumerate(paths)} + + +def _user_guide_index_order() -> list[str]: + _, body = _front_matter(DOCS_ROOT / "user/guide/index.md") + guide_root = (DOCS_ROOT / "user/guide").resolve() + paths: list[str] = [] + for target in MARKDOWN_LINK.findall(body): + resolved = (guide_root / target).resolve() + if resolved.parent != guide_root or resolved.name == "index.md": + continue + relative_path = resolved.relative_to(DOCS_ROOT).as_posix() + if relative_path not in paths: + paths.append(relative_path) + return paths + + +@cache +def _prik_cli_help() -> str: + commands = [ + ["--help"], + ["input.f90", "--help"], + ["parse", "--help"], + ["semantics", "--help"], + ["generate", "--help"], + ["probe", "--help"], + ] + outputs = [] + for command in commands: + result = subprocess.run( + [sys.executable, "-m", "prik", *command], + cwd=ROOT, + capture_output=True, + text=True, + check=True, + ) + outputs.append(result.stdout) + return "\n".join(outputs) + + +def _feature_matrix_rows() -> list[dict[str, str]]: + header = "| Feature | Status | User docs | Source owner | Evidence | Limitations |" + columns = ["Feature", "Status", "User docs", "Source owner", "Evidence", "Limitations"] + rows: list[dict[str, str]] = [] + in_table = False + + for line in FEATURE_MATRIX_PATH.read_text(encoding="utf-8").splitlines(): + if line == header: + in_table = True + continue + if not in_table: + continue + if line.startswith("| ---"): + continue + if not line.startswith("|"): + in_table = False + continue + + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + assert len(cells) == len(columns), f"invalid feature matrix row: {line!r}" + rows.append(dict(zip(columns, cells, strict=True))) + + return rows + + +FEATURE_MATRIX_ROWS = _feature_matrix_rows() diff --git a/tests/shared/docs/test_examples.py b/tests/docs/test_examples.py similarity index 99% rename from tests/shared/docs/test_examples.py rename to tests/docs/test_examples.py index 140ea7c63..33e8f1384 100644 --- a/tests/shared/docs/test_examples.py +++ b/tests/docs/test_examples.py @@ -17,7 +17,7 @@ from prik import pyi_text_to_semantic_module -ROOT = Path(__file__).parents[3] +ROOT = Path(__file__).parents[2] DOC_PATHS = [ ROOT / "README.md", ROOT / "examples/blas/README.md", diff --git a/tests/docs/test_metadata_and_visibility.py b/tests/docs/test_metadata_and_visibility.py new file mode 100644 index 000000000..49f1a380d --- /dev/null +++ b/tests/docs/test_metadata_and_visibility.py @@ -0,0 +1,68 @@ +"""Documentation metadata and visibility contracts.""" + +from pathlib import Path + +import pytest + +from tests.docs._structure_support import ( + ALLOWED_PUBLICATION_STATES, + ALLOWED_STATUSES, + DEFERRED_C_PAGE_PATHS, + DOC_PATHS, + REQUIRED_METADATA, + ROOT, + TODO_STATUSES, + VISIBLE_C_DOCUMENTATION, + VISIBLE_C_DOCUMENTATION_EXCEPTIONS, + WEBSITE_DOCUMENTATION_PATHS, + _front_matter, + _visible_documentation_source, +) + + +@pytest.mark.parametrize("path", DOC_PATHS, ids=lambda path: str(path.relative_to(ROOT))) +def test_documentation_page_metadata(path: Path) -> None: + metadata, body = _front_matter(path) + missing = REQUIRED_METADATA - metadata.keys() + assert not missing, f"{path.relative_to(ROOT)}: missing metadata fields: {sorted(missing)}" + + for key in REQUIRED_METADATA: + assert metadata[key], f"{path.relative_to(ROOT)}: metadata field {key!r} is empty" + + assert metadata["status"] in ALLOWED_STATUSES, f"{path.relative_to(ROOT)}: unknown status {metadata['status']!r}" + assert metadata["publication"] in ALLOWED_PUBLICATION_STATES, ( + f"{path.relative_to(ROOT)}: unknown publication state {metadata['publication']!r}" + ) + if metadata["status"] in TODO_STATUSES: + assert "## TODO" in body, f"{path.relative_to(ROOT)}: unfinished pages must include a TODO section" + assert "TODO:" in body, f"{path.relative_to(ROOT)}: TODO section must contain explicit TODO markers" + + +@pytest.mark.parametrize( + "path", + [ + ROOT / "README.md", + *(path for path in WEBSITE_DOCUMENTATION_PATHS if _front_matter(path)[0].get("publication") == "reviewed"), + ], + ids=lambda path: str(path.relative_to(ROOT)), +) +def test_deferred_c_documentation_is_not_visible(path: Path) -> None: + visible = _visible_documentation_source(path) + for allowed_text in VISIBLE_C_DOCUMENTATION_EXCEPTIONS.get(str(path.relative_to(ROOT)), ()): + visible = visible.replace(allowed_text, "") + match = VISIBLE_C_DOCUMENTATION.search(visible) + assert match is None, f"{path.relative_to(ROOT)}: visible deferred documentation: {match.group(0)!r}" + + +@pytest.mark.parametrize("path", DEFERRED_C_PAGE_PATHS, ids=lambda path: str(path.relative_to(ROOT))) +def test_dedicated_deferred_c_pages_have_no_visible_body(path: Path) -> None: + assert _visible_documentation_source(path).strip() == "" + + +def test_deferred_c_pages_are_not_in_site_navigation() -> None: + lines = (ROOT / "mkdocs.yml").read_text(encoding="utf-8").splitlines() + active_navigation = "\n".join(line for line in lines if not line.lstrip().startswith("#")) + assert "Inspect a C API" not in active_navigation + assert "C Parser Reference" not in active_navigation + assert any("PRIK_C_DOCS" in line and "inspect-c-api.md" in line for line in lines) + assert any("PRIK_C_DOCS" in line and "c-parser-reference.md" in line for line in lines) diff --git a/tests/docs/test_navigation.py b/tests/docs/test_navigation.py new file mode 100644 index 000000000..7a700c306 --- /dev/null +++ b/tests/docs/test_navigation.py @@ -0,0 +1,381 @@ +"""Documentation lane and navigation contracts.""" + +from pathlib import Path + +import pytest + +from tests.docs._structure_support import ( + ALLOWED_CONTEXTUAL_FORWARD_LINK_PREFIXES, + ALLOWED_CONTEXTUAL_FORWARD_LINK_SOURCE_PREFIXES, + DOCS_ROOT, + DOCUMENTATION_CHECKLIST_PATH, + EXAMPLE_DOCUMENTATION_PAGES, + LEARNING_DOCUMENTATION_PATHS, + MARKDOWN_LINK, + REAL_LIBRARY_EXAMPLE_PAGES, + REQUIRED_AREA_INDEXES, + REQUIRED_GETTING_STARTED_PAGES, + REQUIRED_REFERENCE_PAGES, + REQUIRED_ROADMAP_PAGES, + REQUIRED_USER_GUIDE_PAGES, + ROOT, + _front_matter, + _instructional_body_without_next, + _next_navigation_items, + _site_navigation_positions, + _user_guide_index_order, + _visible_documentation_source, +) + + +def test_readme_follows_one_points_workflow_from_build_through_contract_rebuild() -> None: + readme = _visible_documentation_source(ROOT / "README.md") + quick_start = readme.split("## Installation & Quick Start", maxsplit=1)[1].split( + "## How it works", + maxsplit=1, + )[0] + installation_index = quick_start.index("python3 -m pip install prik") + version_index = quick_start.index("prik --version", installation_index) + help_index = quick_start.index("python3 -m prik --help") + source_build_command_index = quick_start.index( + "python3 -m prik points.f90 --out geometry", + help_index, + ) + source_build_tree_index = quick_start.index( + ".\n points.f90\n geometry.so\n __prik__/", + source_build_command_index, + ) + explicit_source_build_command_index = quick_start.index( + "python3 -m prik points.f90 \\\n --out geometry \\\n --out-dir build/geometry", + source_build_tree_index, + ) + explicit_source_build_tree_index = quick_start.index( + "build/geometry/\n geometry..so", + explicit_source_build_command_index, + ) + pyi_generation_command_index = quick_start.index( + "python3 -m prik generate --pyi points.f90 --out contracts", + explicit_source_build_tree_index, + ) + pyi_contract_tree_index = quick_start.index( + "contracts/\n __init__.pyi\n points.pyi", + pyi_generation_command_index, + ) + pyi_contract_body_index = quick_start.index( + "class point:\n" + " def __init__(\n" + " self,\n" + " *,\n" + " x: Float64 = 0.0,\n" + " y: Float64 = 0.0", + pyi_contract_tree_index, + ) + move_contract_index = quick_start.index( + "@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))])\ndef move(", + pyi_contract_body_index, + ) + norm_contract_index = quick_start.index("def norm_squared(", move_contract_index) + pyi_build_command_index = quick_start.index( + "python3 -m prik contracts/__init__.pyi", + norm_contract_index, + ) + native_source_argument_index = quick_start.index("--native-fortran-sources points.f90", pyi_build_command_index) + output_name_index = quick_start.index("--out geometry", native_source_argument_index) + pyi_build_tree_index = quick_start.index("build/geometry_from_pyi/", output_name_index) + import_index = quick_start.index("import geometry.points as points", pyi_build_tree_index) + constructor_index = quick_start.index("item = points.point(", import_index) + mutation_index = quick_start.index("points.move(item", constructor_index) + runtime_output_index = quick_start.index("# 20.0", mutation_index) + verbose_command_index = quick_start.index( + "python3 -m prik points.f90 \\\n --out geometry_debug", + runtime_output_index, + ) + verbose_fortran_flag_index = quick_start.index("--wrapper-fortran-flags=-O2", verbose_command_index) + verbose_c_flag_index = quick_start.index("--wrapper-c-flags=-O2", verbose_fortran_flag_index) + verbose_output_index = quick_start.index("generated Python binding", verbose_c_flag_index) + + assert installation_index < version_index < help_index < source_build_command_index + assert source_build_command_index < source_build_tree_index < explicit_source_build_command_index + assert explicit_source_build_command_index < explicit_source_build_tree_index < pyi_generation_command_index + assert pyi_generation_command_index < pyi_contract_tree_index < pyi_contract_body_index + assert pyi_contract_body_index < move_contract_index < norm_contract_index < pyi_build_command_index + assert pyi_build_command_index < native_source_argument_index < output_name_index < pyi_build_tree_index + assert pyi_build_tree_index < import_index < constructor_index < mutation_index < runtime_output_index + assert runtime_output_index < verbose_command_index + assert verbose_command_index < verbose_fortran_flag_index < verbose_c_flag_index < verbose_output_index + assert "--parse" not in readme + assert "--semantics" not in readme + assert "scale.f90" not in readme + assert "SCALE" not in readme + assert "python3 -m prik solver.f90" not in quick_start + assert "fruntime_abi_f90" not in readme + assert "solver.f90" not in readme + assert "add1" not in readme + assert "distance2" not in readme + assert "point_api" not in readme + assert "build/points" not in readme + assert "tests/data/fortran/general/basic_subroutine.f90" not in readme + assert "contracts/basic_subroutine/basic_subroutine.pyi" not in readme + + +def test_installation_separates_pypi_users_from_editable_contributors() -> None: + installation = _visible_documentation_source(DOCS_ROOT / "user/getting-started/installation.md") + user_section = installation.split("## User Installation", maxsplit=1)[1].split( + "## Contributor Installation", + maxsplit=1, + )[0] + contributor_section = installation.split("## Contributor Installation", maxsplit=1)[1].split( + "## Platform Support", + maxsplit=1, + )[0] + + assert "python3 -m pip install prik" in user_section + assert "pip install -e" not in user_section + assert "git clone https://github.com/PyNumLab/prik.git" in contributor_section + assert 'python3 -m pip install -e ".[qa]"' in contributor_section + + +@pytest.mark.parametrize("relative_path", REQUIRED_AREA_INDEXES) +def test_required_documentation_area_exists(relative_path: str) -> None: + assert (DOCS_ROOT / relative_path).is_file() + + +def test_documentation_root_uses_three_audience_lanes() -> None: + directories = {path.name for path in DOCS_ROOT.iterdir() if path.is_dir()} + root_pages = {path.name for path in DOCS_ROOT.glob("*.md")} + assert directories == { + "user", + "developer", + "maintainer", + "javascripts", + "stylesheets", + "old_docs", + } + assert root_pages == {"index.md"} + + +@pytest.mark.parametrize( + ("lane", "audience_terms"), + [ + ("user", ("users",)), + ("developer", ("developers", "contributors")), + ("maintainer", ("maintainers",)), + ], +) +def test_documentation_lane_has_consistent_audience(lane: str, audience_terms: tuple[str, ...]) -> None: + for path in (DOCS_ROOT / lane).rglob("*.md"): + metadata, _ = _front_matter(path) + assert any(term in metadata["audience"] for term in audience_terms) + if lane != "maintainer": + assert "maintainers" not in metadata["audience"] + else: + assert metadata["audience"] == "maintainers" + + +@pytest.mark.parametrize("path", LEARNING_DOCUMENTATION_PATHS, ids=lambda path: str(path.relative_to(ROOT))) +def test_website_documentation_does_not_link_to_maintainer_lane(path: Path) -> None: + maintainer_root = (DOCS_ROOT / "maintainer").resolve() + for target in MARKDOWN_LINK.findall(_visible_documentation_source(path)): + if target.startswith(("http://", "https://", "mailto:")): + continue + resolved = (path.parent / target).resolve() + assert not resolved.is_relative_to(maintainer_root), ( + f"{path.relative_to(ROOT)}: website link enters maintainer lane: {target}" + ) + + +def test_readme_documentation_links_follow_site_navigation_order() -> None: + readme = _visible_documentation_source(ROOT / "README.md") + documentation = readme.split("## Documentation", maxsplit=1)[1].split( + "## Development", + maxsplit=1, + )[0] + positions = _site_navigation_positions() + linked_positions = [ + positions[target.removeprefix("docs/")] + for target in MARKDOWN_LINK.findall(documentation) + if target.startswith("docs/") + ] + assert linked_positions == sorted(linked_positions) + + +@pytest.mark.parametrize("relative_path", REQUIRED_REFERENCE_PAGES) +def test_required_reference_page_exists(relative_path: str) -> None: + assert (DOCS_ROOT / relative_path).is_file() + + +@pytest.mark.parametrize("relative_path", REQUIRED_REFERENCE_PAGES) +def test_reference_page_is_in_site_navigation(relative_path: str) -> None: + site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + assert relative_path in site_configuration + + +@pytest.mark.parametrize("relative_path", REQUIRED_ROADMAP_PAGES) +def test_required_roadmap_page_exists(relative_path: str) -> None: + assert (DOCS_ROOT / relative_path).is_file() + + +def test_site_navigation_includes_all_publishable_lanes_and_excludes_archive() -> None: + site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + assert "old_docs/**" in site_configuration + positions = _site_navigation_positions() + assert "user/index.md" in positions + assert "developer/index.md" in positions + assert "maintainer/README.md" in positions + + +def test_user_guide_navigation_follows_index_reading_order() -> None: + positions = _site_navigation_positions() + navigation_order = [ + path + for path, _ in sorted(positions.items(), key=lambda item: item[1]) + if path.startswith("user/guide/") and path != "user/guide/index.md" + ] + + assert navigation_order == _user_guide_index_order() + + +@pytest.mark.parametrize("relative_path", REQUIRED_GETTING_STARTED_PAGES) +def test_required_getting_started_page_is_maintained_and_navigable(relative_path: str) -> None: + path = DOCS_ROOT / relative_path + assert path.is_file() + metadata, body = _front_matter(path) + assert metadata["status"] == "maintained" + assert relative_path in (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + for target in MARKDOWN_LINK.findall(body): + if target.startswith(("http://", "https://")): + continue + assert (path.parent / target).resolve().exists(), f"{relative_path}: missing link target {target}" + + +@pytest.mark.parametrize("relative_path", REQUIRED_GETTING_STARTED_PAGES) +def test_getting_started_page_is_completed_in_documentation_checklist(relative_path: str) -> None: + checklist = DOCUMENTATION_CHECKLIST_PATH.read_text(encoding="utf-8") + assert f"- [x] `docs/{relative_path}`" in checklist + + +@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) +def test_required_user_guide_page_is_maintained_and_navigable(relative_path: str) -> None: + path = DOCS_ROOT / relative_path + assert path.is_file() + metadata, body = _front_matter(path) + assert metadata["status"] == "maintained" + assert relative_path in (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + for target in MARKDOWN_LINK.findall(body): + if target.startswith(("http://", "https://")): + continue + assert (path.parent / target).resolve().exists(), f"{relative_path}: missing link target {target}" + + +@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) +def test_user_guide_page_is_completed_in_documentation_checklist(relative_path: str) -> None: + checklist = DOCUMENTATION_CHECKLIST_PATH.read_text(encoding="utf-8") + assert f"- [x] `docs/{relative_path}`" in checklist + + +@pytest.mark.parametrize( + "relative_path", + [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:], *EXAMPLE_DOCUMENTATION_PAGES], +) +def test_sequential_user_pages_do_not_link_forward_from_instructional_prose(relative_path: str) -> None: + path = DOCS_ROOT / relative_path + _, body = _front_matter(path) + body = _instructional_body_without_next(body) + positions = _site_navigation_positions() + if relative_path not in positions: + pytest.skip(f"{relative_path}: not active in site navigation") + source_position = positions[relative_path] + for target in MARKDOWN_LINK.findall(body): + target_path = (path.parent / target).resolve() + if not target_path.is_relative_to(DOCS_ROOT): + continue + target_relative = target_path.relative_to(DOCS_ROOT).as_posix() + if target_relative not in positions: + continue + if relative_path.startswith(ALLOWED_CONTEXTUAL_FORWARD_LINK_SOURCE_PREFIXES) and target_relative.startswith( + ALLOWED_CONTEXTUAL_FORWARD_LINK_PREFIXES + ): + continue + assert positions[target_relative] <= source_position, f"{relative_path}: forward link to {target_relative}" + + +@pytest.mark.parametrize( + "relative_path", + [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:]], +) +def test_next_sections_use_linked_bullet_destinations(relative_path: str) -> None: + _, body = _front_matter(DOCS_ROOT / relative_path) + for line_number, item, is_bullet in _next_navigation_items(body): + assert is_bullet, f"{relative_path}:{line_number}: Next content must be a bullet item" + assert MARKDOWN_LINK.search(item), f"{relative_path}:{line_number}: Next item must include a Markdown link" + + +@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) +def test_user_guide_commands_do_not_expose_fixture_paths(relative_path: str) -> None: + page = (DOCS_ROOT / relative_path).read_text(encoding="utf-8") + assert "python3 -m prik tests/" not in page + + +@pytest.mark.parametrize( + "relative_path", + [ + "index.md", + "user/index.md", + *REQUIRED_GETTING_STARTED_PAGES, + *REQUIRED_USER_GUIDE_PAGES, + ], +) +def test_reviewed_user_pages_do_not_expose_internal_evidence(relative_path: str) -> None: + page = _visible_documentation_source(DOCS_ROOT / relative_path) + assert "## Evidence" not in page + assert "## Runtime Evidence" not in page + assert "Runtime tests:" not in page + assert "../../../tests/" not in page + assert "../../tests/" not in page + assert "../tests/" not in page + + +@pytest.mark.parametrize( + "relative_path", + [ + "index.md", + "user/index.md", + *REQUIRED_GETTING_STARTED_PAGES, + *REQUIRED_USER_GUIDE_PAGES, + ], +) +def test_reviewed_user_pages_do_not_contain_editorial_notes(relative_path: str) -> None: + page = _visible_documentation_source(DOCS_ROOT / relative_path).casefold() + for phrase in ( + "i kept your", + "let me know if you want", + "original content had", + "restore/polish", + ): + assert phrase not in page + + +@pytest.mark.parametrize("relative_path", REAL_LIBRARY_EXAMPLE_PAGES) +def test_real_library_examples_share_a_user_facing_structure(relative_path: str) -> None: + page = _visible_documentation_source(DOCS_ROOT / relative_path) + common_sections = [ + "### What this example shows", + "## Versions used", + "## 1. Prepare the repository and toolchain", + "## 4. Run the complete test suite", + "## 5. See how results are validated", + "## 6. Run focused examples", + "## Troubleshooting", + "## Source provenance", + ] + + positions = [page.index(section) for section in common_sections] + assert positions == sorted(positions) + for internal_phrase in ( + "stopping after a successful import", + "fail-closed", + "authoritative public classification", + "complete maintained suite", + "machine constants", + ): + assert internal_phrase not in page diff --git a/tests/shared/docs/test_publication.py b/tests/docs/test_publication.py similarity index 100% rename from tests/shared/docs/test_publication.py rename to tests/docs/test_publication.py diff --git a/tests/docs/test_reference_and_source_map.py b/tests/docs/test_reference_and_source_map.py new file mode 100644 index 000000000..5256eb621 --- /dev/null +++ b/tests/docs/test_reference_and_source_map.py @@ -0,0 +1,232 @@ +"""Public reference and source-navigation synchronization contracts.""" + +import re + +import pytest + +import prik +from tests.docs._structure_support import ( + ARCHIVED_OLD_DOCS, + CLI_HELP_GROUP_HEADINGS, + CLI_REFERENCE_OPTIONS, + CLI_REFERENCE_PATH, + CLI_VISIBLE_HELP_OPTIONS, + DOCS_ROOT, + FEATURE_MATRIX_PATH, + FEATURE_MATRIX_REQUIRED_FEATURES, + FEATURE_MATRIX_ROWS, + FEATURE_MATRIX_STATUSES, + LEGACY_ACTIVE_DOC_REFERENCES, + MAJOR_SOURCE_PACKAGES, + MARKDOWN_LINK, + PACKAGE_READMES, + PACKAGE_README_NAVIGATION_REFERENCES, + PYTHON_API_REFERENCE_PATH, + REQUIRED_EXAMPLE_RECIPE_PAGES, + REQUIRED_SOURCE_NAVIGATION_PAGES, + ROOT, + SOURCE_NAVIGATION_CORPUS, + SOURCE_NAVIGATION_HOTSPOTS, + SOURCE_NAVIGATION_PUBLIC_DOCS, + SOURCE_NAVIGATION_TEST_TARGETS, + _combined_text, + _prik_cli_help, +) + + +PYTHON_SOURCE_REFERENCE = re.compile( + r"`((?:(?:\.\./|[A-Za-z0-9_]+/)*[A-Za-z0-9_]+\.py))`", +) + + +@pytest.mark.parametrize("heading", CLI_HELP_GROUP_HEADINGS) +def test_cli_help_uses_documented_option_groups(heading: str) -> None: + assert heading in _prik_cli_help() + + +@pytest.mark.parametrize("option", CLI_REFERENCE_OPTIONS) +def test_cli_reference_documents_public_option(option: str) -> None: + content = CLI_REFERENCE_PATH.read_text(encoding="utf-8") + assert option in content + + +@pytest.mark.parametrize("option", CLI_VISIBLE_HELP_OPTIONS) +def test_cli_help_exposes_documented_public_option(option: str) -> None: + assert option in _prik_cli_help() + + +@pytest.mark.parametrize("name", sorted(prik.__all__)) +def test_python_api_reference_documents_public_export(name: str) -> None: + content = PYTHON_API_REFERENCE_PATH.read_text(encoding="utf-8") + assert f"`{name}`" in content + + +@pytest.mark.parametrize("relative_path", REQUIRED_SOURCE_NAVIGATION_PAGES) +def test_required_source_navigation_page_exists(relative_path: str) -> None: + assert (DOCS_ROOT / relative_path).is_file() + + +@pytest.mark.parametrize("relative_path", REQUIRED_SOURCE_NAVIGATION_PAGES) +def test_source_navigation_page_is_in_site_navigation(relative_path: str) -> None: + site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + assert relative_path in site_configuration + + +@pytest.mark.parametrize("relative_path", REQUIRED_EXAMPLE_RECIPE_PAGES) +def test_required_example_recipe_exists(relative_path: str) -> None: + assert (DOCS_ROOT / relative_path).is_file() + + +@pytest.mark.parametrize("relative_path", REQUIRED_EXAMPLE_RECIPE_PAGES) +def test_example_recipe_is_in_site_navigation(relative_path: str) -> None: + site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + assert relative_path in site_configuration + + +@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_HOTSPOTS) +def test_source_navigation_hotspot_exists(relative_path: str) -> None: + assert (ROOT / relative_path).exists() + + +@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_HOTSPOTS) +def test_source_navigation_mentions_hotspot(relative_path: str) -> None: + assert relative_path in _combined_text(SOURCE_NAVIGATION_CORPUS) + + +@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_PUBLIC_DOCS) +def test_source_navigation_public_doc_exists(relative_path: str) -> None: + assert (ROOT / relative_path).is_file() + + +@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_PUBLIC_DOCS) +def test_source_navigation_mentions_public_doc(relative_path: str) -> None: + assert relative_path in _combined_text(SOURCE_NAVIGATION_CORPUS) + + +@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_TEST_TARGETS) +def test_source_navigation_test_target_exists(relative_path: str) -> None: + assert (ROOT / relative_path).exists() + + +@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_TEST_TARGETS) +def test_source_navigation_mentions_test_target(relative_path: str) -> None: + assert relative_path in _combined_text(SOURCE_NAVIGATION_CORPUS) + + +@pytest.mark.parametrize("relative_path", PACKAGE_READMES) +def test_package_readme_links_to_source_navigation(relative_path: str) -> None: + content = (ROOT / relative_path).read_text(encoding="utf-8") + for reference in PACKAGE_README_NAVIGATION_REFERENCES: + assert reference in content + + +@pytest.mark.parametrize("relative_path", PACKAGE_READMES) +def test_package_readme_python_source_references_exist(relative_path: str) -> None: + readme = ROOT / relative_path + references = PYTHON_SOURCE_REFERENCE.findall(readme.read_text(encoding="utf-8")) + + for reference in references: + if reference.startswith(("docs/", "examples/", "prik/", "tests/", "tools/")): + target = ROOT / reference + else: + target = readme.parent / reference + assert target.resolve().is_file(), f"{relative_path}: missing Python source reference {reference}" + + +@pytest.mark.parametrize("relative_path", PACKAGE_READMES) +def test_package_readme_does_not_use_legacy_active_doc_paths(relative_path: str) -> None: + content = (ROOT / relative_path).read_text(encoding="utf-8") + for reference in LEGACY_ACTIVE_DOC_REFERENCES: + assert reference not in content + + +def test_feature_matrix_has_rows_and_status_groups() -> None: + assert FEATURE_MATRIX_ROWS + statuses = {row["Status"] for row in FEATURE_MATRIX_ROWS} + assert {"Supported", "Partially supported", "Unsupported", "Planned", "Not implemented"} <= statuses + + +@pytest.mark.parametrize("feature", FEATURE_MATRIX_REQUIRED_FEATURES) +def test_feature_matrix_includes_required_feature(feature: str) -> None: + matrix_features = {row["Feature"] for row in FEATURE_MATRIX_ROWS} + assert feature in matrix_features + + +@pytest.mark.parametrize("row", FEATURE_MATRIX_ROWS, ids=lambda row: row["Feature"]) +def test_feature_matrix_row_is_complete(row: dict[str, str]) -> None: + assert row["Status"] in FEATURE_MATRIX_STATUSES + assert "TODO" not in " ".join(row.values()) + for column in ["Feature", "Status", "User docs", "Source owner", "Evidence", "Limitations"]: + assert row[column] + for column in ["User docs", "Source owner", "Evidence"]: + assert MARKDOWN_LINK.search(row[column]), f"{row['Feature']}: {column} must contain a Markdown link" + if row["Status"] in {"Supported", "Partially supported"}: + evidence_targets = [ + (FEATURE_MATRIX_PATH.parent / target).resolve() for target in MARKDOWN_LINK.findall(row["Evidence"]) + ] + assert any(target.is_relative_to(ROOT / "tests") for target in evidence_targets), ( + f"{row['Feature']}: support claims need direct test evidence" + ) + + +@pytest.mark.parametrize("row", FEATURE_MATRIX_ROWS, ids=lambda row: row["Feature"]) +def test_feature_matrix_links_point_to_existing_files(row: dict[str, str]) -> None: + for column in ["User docs", "Source owner", "Evidence"]: + for target in MARKDOWN_LINK.findall(row[column]): + if target.startswith(("http://", "https://")): + continue + resolved_target = (FEATURE_MATRIX_PATH.parent / target).resolve() + assert resolved_target.exists(), f"{row['Feature']}: {column} link target does not exist: {target}" + + +@pytest.mark.parametrize("package", MAJOR_SOURCE_PACKAGES) +def test_source_map_covers_major_source_packages(package: str) -> None: + source_map = (DOCS_ROOT / "developer/source-map.md").read_text(encoding="utf-8") + assert package in source_map + + +@pytest.mark.parametrize("relative_path", PACKAGE_READMES) +def test_major_source_package_has_local_readme(relative_path: str) -> None: + assert (ROOT / relative_path).is_file() + + +@pytest.mark.parametrize("relative_path", ARCHIVED_OLD_DOCS) +def test_old_documentation_is_archived(relative_path: str) -> None: + assert (DOCS_ROOT / relative_path).is_file() + + +def test_static_site_seed_configuration_exists() -> None: + assert (ROOT / "mkdocs.yml").is_file() + + +def test_site_theme_keeps_sidebar_open_and_code_blocks_copyable() -> None: + site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + assert "name: readthedocs" in site_configuration + assert "collapse_navigation: false" in site_configuration + assert "navigation_depth: 4" in site_configuration + assert "stylesheets/site.css" in site_configuration + assert "stylesheets/code-copy.css" in site_configuration + assert "javascripts/code-copy.js" in site_configuration + + script = (DOCS_ROOT / "javascripts" / "code-copy.js").read_text(encoding="utf-8") + layout_stylesheet = (DOCS_ROOT / "stylesheets" / "site.css").read_text(encoding="utf-8") + stylesheet = (DOCS_ROOT / "stylesheets" / "code-copy.css").read_text(encoding="utf-8") + assert ".wy-nav-content" in layout_stylesheet + assert "max-width: 1200px" in layout_stylesheet + assert "margin: 0" in layout_stylesheet + assert ".wy-nav-side" in layout_stylesheet + assert "padding-bottom: 0" in layout_stylesheet + assert ".wy-side-scroll" in layout_stylesheet + assert "overflow-y: auto" in layout_stylesheet + assert "scrollbar-width: thin" in layout_stylesheet + assert ".wy-side-scroll::-webkit-scrollbar-thumb" in layout_stylesheet + assert ".rst-versions" in layout_stylesheet + assert "display: none" in layout_stylesheet + assert ".rst-content pre" in layout_stylesheet + assert "width: 100%" in layout_stylesheet + assert "max-width: 56rem" in layout_stylesheet + assert "padding-right: 3.25rem" in layout_stylesheet + assert 'document.querySelectorAll("pre code")' in script + assert "navigator.clipboard.writeText" in script + assert 'button.setAttribute("aria-label", "Copy code to clipboard")' in script + assert ".prik-code-copy" in stylesheet diff --git a/tests/docs/test_user_content.py b/tests/docs/test_user_content.py new file mode 100644 index 000000000..a1b3e43a7 --- /dev/null +++ b/tests/docs/test_user_content.py @@ -0,0 +1,457 @@ +"""User-facing documentation journey and content contracts.""" + +import re + +from tests.docs._structure_support import ( + CLI_REFERENCE_PATH, + DOCS_ROOT, + DOC_PATHS, + MARKDOWN_LINK, + REQUIRED_GETTING_STARTED_PAGES, + REQUIRED_USER_GUIDE_PAGES, + ROOT, + _front_matter, + _visible_documentation_source, +) + + +def test_getting_started_overview_uses_standalone_example() -> None: + overview = (DOCS_ROOT / "user/getting-started/index.md").read_text(encoding="utf-8") + introduction_index = overview.index("you will create\n`scale.f90`") + import_index = overview.index("import scale") + call_index = overview.index("scale.scale(np.float64(3.0), np.float64(2.5))") + + assert introduction_index < import_index < call_index + assert "[Your First Function](first-wrapped-function.md)" in overview + + +def test_documentation_homepage_demonstrates_prik_before_getting_started() -> None: + page = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") + introduction_index = page.index("Generate native Python bindings for Fortran") + example_heading_index = page.index("## From Fortran to Python in one command") + source_index = page.index("```fortran", example_heading_index) + build_index = page.index("python3 -m prik scale.f90", source_index) + import_index = page.index("import scale", build_index) + call_index = page.index("scale.scale(np.float64(3.0), np.float64(2.5))", import_index) + result_index = page.index("# 7.5", call_index) + advantages_index = page.index("## Why PRIK", result_index) + evidence_index = page.index("## Proven on real Fortran libraries", advantages_index) + performance_index = page.index("## Measured against NumPy's f2py", evidence_index) + runtime_chart_index = page.index("user/assets/performance-comparison.svg", performance_index) + build_chart_index = page.index("user/assets/build-time-comparison.svg", runtime_chart_index) + methodology_index = page.index("[See the benchmark machine, full results, and methodology →]", build_chart_index) + install_index = page.index("[Install PRIK →]", methodology_index) + getting_started_index = page.index("[Read Getting Started →]", install_index) + + assert introduction_index < example_heading_index < source_index < build_index + assert build_index < import_index < call_index < result_index + assert result_index < advantages_index < evidence_index < performance_index + assert performance_index < runtime_chart_index < build_chart_index + assert build_chart_index < methodology_index < install_index < getting_started_index + assert "python3 -m pip install prik" in page + assert "No manual binding code is required." in page + assert "[BLAS](user/examples/blas-wrapper.md)" in page + assert "[LAPACK](user/examples/lapack-wrapper.md)" in page + assert "[FFTPACK](user/examples/fftpack-wrapper.md)" in page + assert "[MINPACK](user/examples/minpack-wrapper.md)" in page + assert "The charts show the current published snapshot" in page + assert "specific to its machine and toolchain" in page + assert "PRIK was faster in" not in page + assert page.count("{ .prik-performance-chart }") == 2 + assert page.count("{ .prik-primary-cta }") == 2 + assert "developer/index.md" not in page + assert "maintainer/README.md" not in page + assert "user/guide/" not in page + + +def test_public_entrypoints_use_the_canonical_identity_and_description() -> None: + homepage = _visible_documentation_source(DOCS_ROOT / "index.md") + readme = _visible_documentation_source(ROOT / "README.md") + mkdocs = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + title = "PRIK — Python Runtime Interop Kit" + subtitle = "Generate native Python bindings for Fortran, with editable `.pyi` contracts\nand Pythonic APIs." + description = ( + "PRIK generates native Python bindings from Fortran projects, producing\n" + "importable extensions and editable `.pyi` contracts for Pythonic APIs." + ) + language_direction = ( + "**PRIK starts with Fortran-to-Python.** Its semantic contract model is designed\n" + "to support more native languages over time." + ) + metadata_description = description.replace("\n", " ").replace("`", "") + + assert readme.startswith(f"# {title}\n\n**{subtitle}**\n\n{description}\n") + assert f"# {title}\n\n**{subtitle}**\n\n{description}\n" in homepage + assert language_direction in readme + assert language_direction in homepage + assert f"site_name: {title}" in mkdocs + assert f"site_description: {metadata_description}" in mkdocs + assert f'description = "{metadata_description}"' in pyproject + + +def test_faq_routes_search_questions_to_authoritative_pages() -> None: + metadata, page = _front_matter(DOCS_ROOT / "user/faq/index.md") + question_targets = [ + ( + "How do I call Fortran from Python?", + "how-do-i-call-fortran-from-python", + "../getting-started/first-wrapped-function.md", + ), + ( + "How do I generate Python bindings for a Fortran module?", + "how-do-i-generate-python-bindings-for-a-fortran-module", + "../getting-started/first-wrapped-module.md", + ), + ( + "How do I wrap an existing Fortran library for Python?", + "how-do-i-wrap-an-existing-fortran-library-for-python", + "../guide/building-shared-library.md", + ), + ( + "How do I expose Fortran derived types as Python classes?", + "how-do-i-expose-fortran-derived-types-as-python-classes", + "../guide/wrapping-derived-types.md", + ), + ( + "How do I pass NumPy arrays to Fortran without unnecessary copies?", + "how-do-i-pass-numpy-arrays-to-fortran-without-unnecessary-copies", + "../guide/arrays.md", + ), + ("Should I use PRIK or f2py?", "should-i-use-prik-or-f2py", "../performance.md"), + ] + + assert metadata["status"] == "maintained" + assert metadata["publication"] == "reviewed" + previous_start = -1 + for question, anchor_id, target in question_targets: + start_marker = f'
' + start = page.index(start_marker) + end = page.index("
", start) + answer = page[start:end] + assert previous_start < start + assert f"{question}" in answer + assert target in answer + previous_start = start + + assert page.count('
None: + configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + stylesheet = (DOCS_ROOT / "stylesheets/site.css").read_text(encoding="utf-8") + script = (DOCS_ROOT / "javascripts/faq.js").read_text(encoding="utf-8") + + assert "- md_in_html" in configuration + assert "- javascripts/faq.js" in configuration + assert ".prik-faq-item summary" in stylesheet + assert ".prik-faq-item:target" in stylesheet + assert 'target.matches("details.prik-faq-item")' in script + assert "target.open = true" in script + assert 'window.addEventListener("hashchange", openLinkedQuestion)' in script + + +def test_performance_page_bounds_the_prik_f2py_decision() -> None: + page = _visible_documentation_source(DOCS_ROOT / "user/performance.md") + start = page.index("## Should I use PRIK or f2py?") + end = page.index("## Fair, Like-for-Like Setup", start) + comparison = " ".join(page[start:end].split()) + + assert "runtime-call overhead and clean build time" in comparison + assert "They do not rank feature coverage" in comparison + assert "https://numpy.org/doc/stable/f2py/" in comparison + assert "https://numpy.org/doc/stable/f2py/signature-file.html" in comparison + assert "design the Python API, not just generate a wrapper" in comparison + assert "simpler, more Pythonic place to rename or hide exports, flatten modules" in comparison + assert "reorder or hide native arguments, and return native outputs as Python results" in comparison + assert "[NumPy arrays](guide/arrays.md) as complete API contracts" in comparison + assert "dtype, rank, shape, memory layout, contiguity, strides, mutation, and copy behavior" in comparison + assert "[supported positive-stride views](guide/arrays.md#strided-views) without copying" in comparison + assert "[derived types](guide/wrapping-derived-types.md) as Python classes" in comparison + assert "[allocatables](guide/allocatables.md)" in comparison + assert "[pointer forms](guide/pointers.md)" in comparison + assert "native errors as [Python exceptions](guide/error-handling.md)" in comparison + assert "[overloaded procedures](guide/generic-interfaces.md)" in comparison + assert "reference/pyi-contracts/index.md" in comparison + assert "PRIK is currently alpha" in comparison + + +def test_documentation_links_to_documentation_stay_on_the_website() -> None: + github_documentation_prefixes = ( + "https://github.com/PyNumLab/prik/blob/main/docs/", + "https://github.com/PyNumLab/prik/tree/main/docs/", + ) + + for path in DOC_PATHS: + prose_lines: list[str] = [] + fence: str | None = None + for line in _visible_documentation_source(path).splitlines(): + marker = re.match(r"^\s*(`{3,}|~{3,})", line) + if fence is not None: + if line.strip() == fence: + fence = None + continue + if marker is not None: + fence = marker.group(1) + continue + prose_lines.append(line) + + for target in MARKDOWN_LINK.findall("\n".join(prose_lines)): + if "/" not in target and "." not in target: + continue + assert not target.startswith(github_documentation_prefixes), ( + f"{path.relative_to(ROOT)}: documentation link points to GitHub: {target}" + ) + if target.startswith(("http://", "https://", "mailto:")): + continue + resolved = (path.parent / target).resolve() + assert resolved != (ROOT / "README.md").resolve(), ( + f"{path.relative_to(ROOT)}: documentation workflow points to the repository README" + ) + if resolved.is_relative_to(DOCS_ROOT.resolve()): + assert resolved.is_file(), ( + f"{path.relative_to(ROOT)}: documentation link must target a website page or asset: {target}" + ) + + +def test_first_wrapped_function_shows_contract_and_mentions_later_support_boundaries() -> None: + page = (DOCS_ROOT / "user/getting-started/first-wrapped-function.md").read_text(encoding="utf-8") + source_index = page.index("scale.f90") + build_index = page.index("python3 -m prik scale.f90") + command_index = page.index("python3 -m prik generate --pyi scale.f90") + contract_index = page.index( + "@standalone\n@native_call([Addr(Arg(0)), Addr(Arg(1))])\ndef scale(\n" + " value: Float64,\n factor: Float64\n) -> Float64: ..." + ) + docstring_index = page.index("## Inspect the Generated Docstring") + call_index = page.index("## Call the Function") + + assert source_index < command_index < contract_index < build_index + assert build_index < docstring_index < call_index + assert "editable description of the\nPython interface" in page + assert "scale(value, factor) -> float64" in page + assert "assert result == 7.5" in page + assert "isinstance(result, float)" not in page + assert "## Current Limitations" not in page + + +def test_first_wrapped_module_shows_local_input_and_generated_contract() -> None: + page = (DOCS_ROOT / "user/getting-started/first-wrapped-module.md").read_text(encoding="utf-8") + source_index = page.index("module_state.f90") + build_index = page.index("python3 -m prik module_state.f90") + docstring_index = page.index("## Inspect the Generated Docstring") + usage_index = page.index("## Usage Example") + inspect_index = page.index("python3 -m prik generate --pyi module_state.f90") + contract_index = page.index("## Key Rules") + + assert source_index < build_index < docstring_index < usage_index < inspect_index < contract_index + assert "print(mod.__doc__)" in page + assert "module_state\n\nModule Attributes" in page + assert "summarize() -> int32" in page + assert "scaled_counter() -> float64" in page + assert "next_local() -> int32" in page + assert "nmax : int32\n Read-only constant." in page + assert "counter : int32" in page + assert "scale : float64" in page + assert "saved_counter : int32" in page + assert "Assignment writes through to native storage." not in page + assert "fmodule_vars_f90" not in page + assert "## Current Limitations" not in page + + +def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: + page = (DOCS_ROOT / "user/getting-started/beginner-workflow.md").read_text(encoding="utf-8") + source_reference_index = page.index("scale.f90") + layout_index = page.index("src/") + contract_index = page.index("python3 -m prik generate --pyi src/scale.f90") + build_index = page.index("python3 -m prik src/scale.f90") + smoke_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") + editing_index = page.index("## 4. Optionally Edit the Contract") + edited_contract_index = page.index("contracts/scale/__init__.pyi", editing_index) + diagnosis_index = page.index("## 5. Diagnose a Failure") + + assert source_reference_index < layout_index < contract_index < build_index + assert build_index < smoke_index < editing_index < edited_contract_index < diagnosis_index + assert "[First Wrapped Function](first-wrapped-function.md)" in page + assert "scale_api" not in page + + +def test_user_guide_teaches_small_contract_edits_in_context() -> None: + arrays = _visible_documentation_source(DOCS_ROOT / "user/guide/arrays.md") + strings = _visible_documentation_source(DOCS_ROOT / "user/guide/strings.md") + functions = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-functions.md") + modules = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-modules.md") + generics = _visible_documentation_source(DOCS_ROOT / "user/guide/generic-interfaces.md") + derived = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-derived-types.md") + errors = _visible_documentation_source(DOCS_ROOT / "user/guide/error-handling.md") + + assert "Edit the semantic `.pyi` and add `ORDER_C`" in arrays + assert "Edit the declarations in `contracts/strings/strings_api.pyi`" in strings + assert '@bind("scale")' in functions + assert "## Shape the Module API With the Contract" in modules + assert "nmax: Final[Int32] = 12" in modules + assert "counter: Int32 = 9" in modules + assert "scale: Float64 = 2.0" in modules + assert "saved_counter: private[Int32]" in modules + assert "It does not turn a writable Fortran variable into a read-only" in modules + assert "## Flatten Module Namespaces" in modules + assert "from .module1 import *" in modules + assert "from .module2 import *" in modules + assert "library.func1()" in modules + assert "library.module1` and `library.module2` are no longer exported" in modules + assert "the wrapper build fails and asks for an explicit" in modules + assert "## Extend an Overload Set" in generics + assert '@overload("convert_logical")' in generics + assert "## Custom Constructor" in derived + assert '@bind("initialize_point")' in derived + assert "### Expose a Module Procedure as a Method" in derived + assert "def move(self, dx: Float64, dy: Float64)" in derived + assert '@raises(status="status", message="message", success=0)' in errors + + +def test_array_guide_routes_advanced_shape_expressions_to_reference() -> None: + guide = _visible_documentation_source(DOCS_ROOT / "user/guide/arrays.md") + reference = _visible_documentation_source(DOCS_ROOT / "user/reference/pyi-contracts/calls-and-results.md") + + assert "Generated contracts may describe a shape with visible arguments" in guide + assert "../reference/pyi-contracts/calls-and-results.md#advanced-array-shape-expressions" in guide + assert "## Advanced Array Shape Expressions" in reference + assert "Native relationship" in reference + assert "extent_for(n)" in reference + + assert "The bridge emits the signature as an abstract Fortran interface" not in guide + assert "A specification function must be pure" not in guide + assert "compiler-produced `.mod` interfaces" not in guide + + +def test_user_guide_keeps_generated_docstrings_with_new_overload_and_class_features() -> None: + modules = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-modules.md") + generics = _visible_documentation_source(DOCS_ROOT / "user/guide/generic-interfaces.md") + derived = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-derived-types.md") + + assert "## Inspect the Module" not in modules + assert "print(mod.__doc__)" not in modules + + assert "## Inspect the Overloads" in generics + assert "print(conversions.__doc__)" in generics + assert "print(conversions.convert.__doc__)" in generics + assert "convert(value: int32) -> int32" in generics + assert "convert(value: float64) -> float64" in generics + + assert "## Inspect the Class" in derived + assert "print(points.point.__doc__)" in derived + assert "print(points.point.__init__.__doc__)" in derived + assert "`points.point.move.__doc__`" in derived + assert "print(points.point.__add__.__doc__)" in derived + assert "__add__(right: point) -> point" in derived + + +def test_getting_started_pages_keep_advanced_stage_flags_out_of_beginner_path() -> None: + content = "\n".join( + _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_GETTING_STARTED_PAGES + ) + + assert "--parse" not in content + assert "--semantics" not in content + assert "--json" not in content + + +def test_user_guide_shows_direct_shared_library_build() -> None: + content = "\n".join( + _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_USER_GUIDE_PAGES + ) + shared_library = _visible_documentation_source(DOCS_ROOT / "user/guide/building-shared-library.md") + + assert "python3 -m prik src/scale.f90 --out-dir build/scale" in content + assert "[Common Beginner Workflow](../getting-started/beginner-workflow.md)" in shared_library + + +def test_user_compiler_docs_distinguish_runtime_evidence_from_configured_profiles() -> None: + installation = _visible_documentation_source(DOCS_ROOT / "user/getting-started/installation.md") + shared_library = _visible_documentation_source(DOCS_ROOT / "user/guide/building-shared-library.md") + + for compiler_pair in ( + "| `gfortran` | `gcc` | Default; tested on Linux and macOS |", + "| `ifx` | `icx` | Tested on Linux with version 2026.1.1 |", + "| `flang` | `clang` | Tested on Linux and macOS with version 22.1.8 |", + ): + assert compiler_pair in installation + + assert "| `ifort` | `icx` | Recognized; not routinely tested |" in installation + assert "| `nvfortran` | `nvc` | Recognized; not yet tested |" in installation + assert "| `pgfortran` | `pgcc` | Legacy option; not yet tested |" in installation + assert "For the best-tested experience, use GNU, IFX, or Flang." in installation + assert "--compiler ifx" in shared_library + assert "--compiler flang" in shared_library + assert "keeps both compilers in the same family" in shared_library + + +def test_cli_reference_reuses_the_derived_type_points_example() -> None: + content = _visible_documentation_source(CLI_REFERENCE_PATH) + + assert "`points.f90` and `geometry` naming" in content + assert "../guide/wrapping-derived-types.md#complete-example" in content + assert "python3 -m prik parse points.f90" in content + assert "python3 -m prik generate --pyi points.f90 --out contracts" in content + assert "scale.f90" not in content + + +def test_fortran_wrapper_reference_shows_every_common_shared_library_build_input() -> None: + content = _visible_documentation_source(DOCS_ROOT / "user/reference/fortran-wrapper.md") + example = content.split("For example, this command supplies every common build input", maxsplit=1)[1].split( + "`--compiler` selects", maxsplit=1 + )[0] + + for value in ( + "python3 -m prik solver.f90", + "--out solver", + "--out-dir build/solver", + "--compiler gfortran", + "-I include", + "--native-compile-flags=-O3", + "--native-library openblas", + "--verbose", + ): + assert value in example + + +def test_array_handle_docs_keep_views_copies_and_handles_distinct() -> None: + allocatables = _visible_documentation_source(DOCS_ROOT / "user/guide/allocatables.md") + pointers = _visible_documentation_source(DOCS_ROOT / "user/guide/pointers.md") + memory = _visible_documentation_source(DOCS_ROOT / "user/guide/memory-management.md") + + assert "Reading the Python attribute" in allocatables + assert "returns an `Allocatable[T[...]]` handle, not `ndarray | None`." in allocatables + assert "never creates an automatic detached snapshot" in allocatables + assert "A NumPy view reflects current native storage." in allocatables + assert "A pointer-array function result becomes a returned `PointerArray`." in pointers + assert "has persistent descriptor storage, but the target can belong to another" in pointers + assert "`associate(other)` makes two pointer handles refer to the same target" in pointers + assert "If `p2` is unassociated, `p1` becomes" in pointers + assert "Do Not Return A Pointer To Expired Local Storage" in pointers + assert "Derived module variables remain live objects" in memory + assert "Fortran module owns their storage" in memory + + +def test_parameter_array_references_document_read_only_snapshots() -> None: + references = [ + _visible_documentation_source(DOCS_ROOT / "user/reference/fortran-wrapper.md"), + _visible_documentation_source(DOCS_ROOT / "user/reference/generated-modules.md"), + ] + + for reference in references: + normalized_reference = " ".join(reference.split()) + assert "Python-owned" in normalized_reference + assert "read-only NumPy snapshots" in normalized_reference + assert "no native setter" in normalized_reference diff --git a/tests/fortran/README.md b/tests/fortran/README.md index 6a6e5fa28..08fc412b1 100644 --- a/tests/fortran/README.md +++ b/tests/fortran/README.md @@ -4,9 +4,6 @@ Fortran, including semantic `.pyi` wrapper builds and the generated Fortran/C/CPython implementation of that contract. -Meta-tests that inspect this directory's ownership, permanent evidence ledger, -or smoke selection live outside it under `tests/architecture/fortran/`. - The final organization is feature first and stage second: ```text @@ -71,8 +68,17 @@ evidence, not the ownership rule. | Final directory | Owner | | --- | --- | -| `infrastructure/policy/` | Internal completed-policy dispatch and validation framework | -| `infrastructure/codegen/` | Shared typed-plan and generator mechanics | +| `infrastructure/binding_support/` | Native binding-support package contracts that have no public feature owner | +| `infrastructure/semantics/` | Internal semantic ownership, policy completion, and completed wrapper-policy mechanics, with one test module per production module | +| `infrastructure/codegen/` | Internal plan, planner, generator, binding, bridge, printer, docstring, check, and visitor mechanics, with one test module per production module | +| `infrastructure/naming/` | Internal generated-name and public-name policy owned by `prik/naming/` | +| `infrastructure/pipeline/` | Internal wrapper-artifact transport owned by `prik/pipeline/` | +| `infrastructure/types/` | Internal NumPy type mapping and target mapping-report mechanics | +| `infrastructure/utilities/` | Internal string and class-visitor helpers owned by `prik/utilities/` | + +Each infrastructure test module has an explicit production owner. New internal +coverage goes in the matching `prik/` package directory; it must not recreate a +generic policy, stage, or internals catch-all. Minimized real-source parser regressions live in `source_parsing/parsing/test_real_world_interaction_regressions.py`. A diff --git a/tests/fortran/_support/fixture_conversion.py b/tests/fortran/_support/fixture_conversion.py index 25a09f59c..99839cdce 100644 --- a/tests/fortran/_support/fixture_conversion.py +++ b/tests/fortran/_support/fixture_conversion.py @@ -1,10 +1,6 @@ from tests.fortran._support.fixture_outputs import ( - PARSER_FIXTURE_ROOT as TESTS_DIR, iter_general_fortran_fixtures, - parse_fixture, ) FORTRAN_FIXTURES = iter_general_fortran_fixtures() - -__all__ = ("FORTRAN_FIXTURES", "TESTS_DIR", "parse_fixture") diff --git a/tests/fortran/_support/native_array_handles.py b/tests/fortran/_support/native_array_handles.py index 353927006..d14311abd 100644 --- a/tests/fortran/_support/native_array_handles.py +++ b/tests/fortran/_support/native_array_handles.py @@ -1,30 +1,10 @@ """Shared native-array handle test doubles for Fortran feature tests.""" -import gc - -import ctypes - import numpy as np -import pytest - -import prik from prik.runtime.handles import ( - _NativeArrayDescriptorHandoff, _NativeArrayHandoff, - _native_array_actual_argument_for_binding_positional, - _native_array_actual_for_binding, - _native_array_descriptor_argument_for_binding, - _native_array_descriptor_argument_for_binding_positional, - _native_array_descriptor_for_binding, - _native_array_descriptor_handoff_for_binding, - _native_array_descriptor_handoff_for_binding_positional, - _native_array_handle_from_generated_ops, - _numpy_view_from_pointer_c_descriptor, - AllocatableArray, - NativeArrayHandleBase, - PointerArray, ) @@ -82,32 +62,3 @@ def _pointer_descriptor_record_for_array(value: np.ndarray): rank=descriptor["rank"], dim=[_DescriptorFieldRecord(**dimension) for dimension in descriptor["dim"]], ) - - -__all__ = ( - "AllocatableArray", - "NativeArrayHandleBase", - "PointerArray", - "_ArrayState", - "_NativeArrayDescriptorHandoff", - "_NativeArrayHandoff", - "_common_ops", - "_handoff", - "_native_array_actual_argument_for_binding_positional", - "_native_array_actual_for_binding", - "_native_array_descriptor_argument_for_binding", - "_native_array_descriptor_argument_for_binding_positional", - "_native_array_descriptor_for_binding", - "_native_array_descriptor_handoff_for_binding", - "_native_array_descriptor_handoff_for_binding_positional", - "_native_array_handle_from_generated_ops", - "_numpy_view_from_pointer_c_descriptor", - "_pointer_descriptor_for_array", - "_pointer_descriptor_record_for_array", - "_required_handoff_ops", - "ctypes", - "gc", - "np", - "prik", - "pytest", -) diff --git a/tests/fortran/_support/ownership_policy.py b/tests/fortran/_support/ownership_policy.py index b39287618..1685bf361 100644 --- a/tests/fortran/_support/ownership_policy.py +++ b/tests/fortran/_support/ownership_policy.py @@ -1,74 +1,27 @@ """Shared support for Fortran wrapper policy and lowering tests.""" -import pytest - from prik.contracts import CONTRACT_SYMBOLS from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, - ADDRESS_ROLE_PROJECTION, - ADDRESS_ROLE_RAW, - PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY, ) -from prik.codegen.printers import PyiPrinter from prik.semantics.ownership import ( - AssignmentMode, - CodegenAction, - DestructionPolicy, - NativeBarrierAction, - NativeBarrierDispatcher, - ObjectKind, OwnershipContext, - OwnershipDecision, - OwnershipOwner, - OwnershipPolicyResolver, - PolicyActionDispatcher, - PythonBarrierAction, - PythonBarrierDispatcher, - SetterAction, - StorageMode, - TransferMode, - default_ownership_policy, - set_ownership_metadata, ) from prik.semantics.models import ( - POLICY_COMPLETION_PREPARED_METADATA, - PYTHON_EXPORTS_METADATA, - PYTHON_EXPORTS_PREPARED_METADATA, - RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA, - RESOLVED_MODULE_VARIABLE_INITIALIZER_METADATA, - RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, - RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA, - RESOLVED_OWNERSHIP_POLICY_METADATA, - RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA, - ProjectionMapping, - SemanticArgument, SemanticArrayContract, - SemanticClass, - SemanticConstraint, - SemanticField, - SemanticFunction, - SemanticModule, SemanticStorageContract, SemanticType, - SemanticVariable, ) from prik.semantics.native_array_handles import ( - ArrayInteropPolicy, - ArrayInteropPolicyDispatcher, - NativeArrayBuildRequirement, NativeArrayHandlePolicy, - NativeArrayHandlePolicyDispatcher, - native_array_descriptor_kind, - native_array_handle_build_requirements, ) -from prik.semantics.policy_completion import complete_semantic_policies from prik.pipeline.pyi import pyi_text_to_semantic_module as _parse_pyi_text @@ -199,68 +152,3 @@ def _native_array_policy( storage_mode="alias", operations=operations, ) - - -__all__ = ( - "ADDRESS_ROLE_METADATA", - "ADDRESS_ROLE_PROJECTION", - "ADDRESS_ROLE_RAW", - "POLICY_COMPLETION_PREPARED_METADATA", - "PROJECTED_OUTPUT_METADATA", - "PYTHON_EXPORTS_METADATA", - "PYTHON_EXPORTS_PREPARED_METADATA", - "RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA", - "RESOLVED_MODULE_VARIABLE_INITIALIZER_METADATA", - "RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA", - "RESOLVED_OWNERSHIP_POLICY_METADATA", - "RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA", - "RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA", - "ArrayInteropPolicy", - "ArrayInteropPolicyDispatcher", - "AssignmentMode", - "CodegenAction", - "DestructionPolicy", - "NativeArrayBuildRequirement", - "NativeArrayHandlePolicyDispatcher", - "NativeBarrierAction", - "NativeBarrierDispatcher", - "ObjectKind", - "OwnershipContext", - "OwnershipDecision", - "OwnershipOwner", - "OwnershipPolicyResolver", - "PolicyActionDispatcher", - "ProjectionMapping", - "PyiPrinter", - "PythonBarrierAction", - "PythonBarrierDispatcher", - "SemanticArgument", - "SemanticClass", - "SemanticConstraint", - "SemanticField", - "SemanticFunction", - "SemanticModule", - "SemanticType", - "SemanticVariable", - "SetterAction", - "StorageMode", - "TransferMode", - "_address_type", - "_array_type", - "_derived_type", - "_hidden_output_context", - "_native_array_policy", - "_read_only_argument_context", - "_scalar_storage_type", - "_scalar_type", - "_string_storage_type", - "_string_type", - "_writable_argument_context", - "complete_semantic_policies", - "default_ownership_policy", - "native_array_descriptor_kind", - "native_array_handle_build_requirements", - "parse_pyi_text", - "pytest", - "set_ownership_metadata", -) diff --git a/tests/fortran/_support/parser_procedures.py b/tests/fortran/_support/parser_procedures.py index df3423fb8..660bc1936 100644 --- a/tests/fortran/_support/parser_procedures.py +++ b/tests/fortran/_support/parser_procedures.py @@ -1,8 +1,4 @@ -import pytest - -from prik.parsers.fortran.models import FortranFunctionCall, FortranSlice, FortranUseMapping, FortranVariable - -from prik import FortranParseError, parse_fortran_file, parse_fortran_project +from prik import parse_fortran_file, parse_fortran_project def collect_project_procedure_signatures(files): @@ -109,28 +105,3 @@ def evaluate_signature_shapes(signature, symbol_values=None): dim = re.sub(rf"\b{re.escape(str(k))}\b", str(v), dim) a.shape[i] = dim return out - - -__all__ = ( - "COMPILE_TIME_EXPRESSION_SOURCE", - "FortranFunctionCall", - "FortranParseError", - "FortranSlice", - "FortranUseMapping", - "FortranVariable", - "collect_project_procedure_signatures", - "collect_signature_shape_symbols", - "evaluate_signature_shapes", - "parse_fortran_block_data", - "parse_fortran_block_data_unit", - "parse_fortran_file", - "parse_fortran_interfaces", - "parse_fortran_module", - "parse_fortran_modules", - "parse_fortran_program", - "parse_fortran_programs", - "parse_fortran_project", - "parse_fortran_submodule", - "parse_fortran_submodules", - "pytest", -) diff --git a/tests/fortran/_support/parser_properties.py b/tests/fortran/_support/parser_properties.py index 27d35d43a..64104627b 100644 --- a/tests/fortran/_support/parser_properties.py +++ b/tests/fortran/_support/parser_properties.py @@ -2,33 +2,13 @@ from __future__ import annotations -from contextlib import suppress - -from pathlib import Path - -import sys - -from tempfile import TemporaryDirectory - -from unittest.mock import patch import pytest pytest.importorskip("hypothesis") -from hypothesis import given, strategies as st - -import prik.pipeline.preprocessing as preprocessing - -from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from hypothesis import strategies as st -from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text - -from prik.codegen.printers import emit_module_stubs - -from prik import FortranParseError, parse_fortran_file - -from prik.pipeline.preprocessing import PreprocessingConfig, preprocess_source _FORTRAN_SCALAR_TYPES = st.sampled_from(["integer", "real", "logical"]) @@ -54,27 +34,3 @@ def fortran_subroutines(draw): lines.append(f"end subroutine {proc_name}") return proc_name, arg_names, "\n".join(lines) + "\n" - - -__all__ = ( - "_FORTRAN_IDENTIFIER_STEMS", - "_FORTRAN_SCALAR_TYPES", - "_FUZZ_TEXT", - "FortranParseError", - "Path", - "PreprocessingConfig", - "TemporaryDirectory", - "emit_module_stubs", - "fortran_file_to_semantic_modules", - "fortran_subroutines", - "given", - "parse_fortran_file", - "parse_pyi_text", - "patch", - "preprocess_source", - "preprocessing", - "pytest", - "st", - "suppress", - "sys", -) diff --git a/tests/fortran/_support/parser_regressions.py b/tests/fortran/_support/parser_regressions.py index 0311d13c6..11ef285bd 100644 --- a/tests/fortran/_support/parser_regressions.py +++ b/tests/fortran/_support/parser_regressions.py @@ -2,23 +2,12 @@ from __future__ import annotations -from pathlib import Path - -import pytest - -from prik.parsers.fortran.models import FortranArgument, FortranDerivedType, FortranModule, FortranProcedureSignature from prik.parsers.fortran.parser import ( - FortranParser, SourceUnit, - _ParserScope, _SOURCE_UNIT_TYPES, - _UnitParts, - parse_fortran_project, ) -from prik import FortranParseError, parse_fortran_file - def _lines(*values: str) -> list[tuple[str, int, str]]: return [(value, lineno, value) for lineno, value in enumerate(values, start=1)] @@ -31,22 +20,3 @@ def _unit(kind: str, name: str | None, *values: str) -> SourceUnit: def _empty_unit(kind: str, name: str | None, start_line: int | None, end_line: int | None) -> SourceUnit: return _SOURCE_UNIT_TYPES[kind](kind, name, [], start_line, end_line) - - -__all__ = ( - "FortranArgument", - "FortranDerivedType", - "FortranModule", - "FortranParseError", - "FortranParser", - "FortranProcedureSignature", - "Path", - "_ParserScope", - "_UnitParts", - "_empty_unit", - "_lines", - "_unit", - "parse_fortran_file", - "parse_fortran_project", - "pytest", -) diff --git a/tests/fortran/_support/printer_models.py b/tests/fortran/_support/printer_models.py index 6a5e98fba..a86917908 100644 --- a/tests/fortran/_support/printer_models.py +++ b/tests/fortran/_support/printer_models.py @@ -1,8 +1,5 @@ from pathlib import Path -import pytest - -import prik from prik.contracts import CONTRACT_SYMBOLS @@ -17,31 +14,11 @@ from prik.codegen.printers import ( emit_module, - emit_module_stubs, - opaque_dependency_modules, - PyiPrinter, ) from prik.codegen import WrapperCodeGenerator, WrapperPlanner from prik.semantics.models import ( - PROTOTYPE_REF_METADATA, - ProjectionMapping, - RUNTIME_RELEASE_GIL_METADATA, - RUNTIME_STATUS_ERROR_METADATA, - SemanticArgument, - SemanticArrayContract, - SemanticClass, - SemanticConstraint, - SemanticImport, - SemanticMethod, SemanticModule, - SemanticOrigin, - SemanticPrototype, - SemanticFunction, - SemanticField, - SemanticStorageContract, - SemanticType, - SemanticVariable, ) from prik.semantics.policy_completion import complete_semantic_policies @@ -82,42 +59,3 @@ def rendered_source(artifacts, suffix: str) -> str: def normalize(text: str) -> str: return "\n".join(line.rstrip() for line in text.strip().splitlines()) - - -__all__ = ( - "OPERATOR_F90_SOURCE", - "PROTOTYPE_REF_METADATA", - "RUNTIME_RELEASE_GIL_METADATA", - "RUNTIME_STATUS_ERROR_METADATA", - "Path", - "ProjectionMapping", - "PyiPrinter", - "SemanticArgument", - "SemanticArrayContract", - "SemanticClass", - "SemanticConstraint", - "SemanticField", - "SemanticFunction", - "SemanticImport", - "SemanticMethod", - "SemanticModule", - "SemanticOrigin", - "SemanticPrototype", - "SemanticStorageContract", - "SemanticType", - "SemanticVariable", - "_parse_pyi_text", - "complete_semantic_policies", - "emit_module", - "emit_module_stubs", - "fortran_module_to_semantic_module", - "generate_pyi", - "generate_wrapper_artifacts", - "normalize", - "opaque_dependency_modules", - "parse_fortran_source", - "parse_pyi_text", - "prik", - "pytest", - "rendered_source", -) diff --git a/tests/fortran/_support/pyi_conversion.py b/tests/fortran/_support/pyi_conversion.py index 42b903371..23b9fccf8 100644 --- a/tests/fortran/_support/pyi_conversion.py +++ b/tests/fortran/_support/pyi_conversion.py @@ -1,71 +1,8 @@ -import ast - -import re - -from dataclasses import asdict - -from pathlib import Path - -import pytest - -import prik.pipeline.pyi as pyi_pipeline - from prik.contracts import CONTRACT_SYMBOLS -from prik.semantics.metadata import ( - ADDRESS_ROLE_METADATA, - ADDRESS_ROLE_PROJECTION, - ADDRESS_ROLE_RAW, - BIND_TARGET_METADATA, - NATIVE_ARRAY_DESCRIPTOR_METADATA, - OPTIONAL_ABSENT_HANDLE_METADATA, - PROJECTED_OUTPUT_METADATA, - SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA, - USER_PRIVATE_METADATA, -) - -from prik.semantics.fortran2ir import fortran_file_to_semantic_modules - -from prik.semantics.native_array_handles import ( - is_native_array_handle, - native_array_data_type, - native_array_descriptor_kind, - native_array_handle_facts, -) - -from prik.semantics.models import ( - ProjectionMapping, - PYTHON_VALUE_IMMUTABLE, - PYTHON_VALUE_MUTABILITY_METADATA, - SemanticArgument, - SemanticConstraint, - SemanticField, - SemanticFunction, - SemanticImport, - SemanticImportItem, - SemanticModule, - SemanticType, - SemanticVariable, -) -from prik.semantics.pyi2ir import ( - _PyiAstParser, - _node_text, - convert_pyi_to_ir, -) +from prik.pipeline.pyi import pyi_text_to_semantic_module -from prik.pipeline.pyi import pyi_file_to_semantic_module, pyi_paths_to_semantic_modules, pyi_text_to_semantic_module - -from prik.parsers.pyi import parse_pyi_text as parse_pyi_ast_text - -from prik.semantics.native_contract import native_contract_issues - -from prik.semantics.policy_completion import complete_semantic_policies - - -from prik.codegen.printers import emit_module - -from prik import parse_fortran_file CONTRACT_IMPORT = f"from prik.contracts import {', '.join(sorted(CONTRACT_SYMBOLS))}\n" @@ -74,53 +11,3 @@ def parse_pyi_text(source: str, *args, **kwargs): if "prik.contracts" in source: return pyi_text_to_semantic_module(source, *args, **kwargs) return pyi_text_to_semantic_module(f"{CONTRACT_IMPORT}{source}", *args, **kwargs) - - -__all__ = ( - "ADDRESS_ROLE_METADATA", - "ADDRESS_ROLE_PROJECTION", - "ADDRESS_ROLE_RAW", - "BIND_TARGET_METADATA", - "CONTRACT_IMPORT", - "CONTRACT_SYMBOLS", - "NATIVE_ARRAY_DESCRIPTOR_METADATA", - "OPTIONAL_ABSENT_HANDLE_METADATA", - "PROJECTED_OUTPUT_METADATA", - "PYTHON_VALUE_IMMUTABLE", - "PYTHON_VALUE_MUTABILITY_METADATA", - "SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA", - "USER_PRIVATE_METADATA", - "Path", - "ProjectionMapping", - "SemanticArgument", - "SemanticConstraint", - "SemanticField", - "SemanticFunction", - "SemanticImport", - "SemanticImportItem", - "SemanticModule", - "SemanticType", - "SemanticVariable", - "_PyiAstParser", - "_node_text", - "asdict", - "ast", - "complete_semantic_policies", - "convert_pyi_to_ir", - "emit_module", - "fortran_file_to_semantic_modules", - "is_native_array_handle", - "native_array_data_type", - "native_array_descriptor_kind", - "native_array_handle_facts", - "native_contract_issues", - "parse_fortran_file", - "parse_pyi_ast_text", - "parse_pyi_text", - "pyi_file_to_semantic_module", - "pyi_paths_to_semantic_modules", - "pyi_pipeline", - "pyi_text_to_semantic_module", - "pytest", - "re", -) diff --git a/tests/fortran/_support/semantic_conversion.py b/tests/fortran/_support/semantic_conversion.py index 02a2ef980..fbc93d5f2 100644 --- a/tests/fortran/_support/semantic_conversion.py +++ b/tests/fortran/_support/semantic_conversion.py @@ -1,66 +1,8 @@ -import json - -import re - -from dataclasses import asdict - -import pytest - -from prik.parsers.fortran.models import ( - FortranArgument, - FortranBlockData, - FortranDerivedType, - FortranFile, - FortranModule, - FortranProgram, - FortranProject, - FortranProcedureSignature, - FortranSubmodule, - FortranUseMapping, - FortranVariable, -) - -from prik import parse_fortran_file as parse_fortran_source - -from prik import parse_fortran_project - -from prik.semantics.fortran2ir import ( - FortranToIRConverter, - _compile_time_requirement_message, - _iter_fortran_variable_contexts, - _requirement_unit_name, - _resolve_compile_time_text, - collect_fortran_type_storage_requirements, - collect_semantic_compile_time_requirements, - fortran_type_storage_expression, - fortran_file_to_semantic_modules, - fortran_module_to_semantic_module, - fortran_project_to_semantic_modules, - resolve_semantic_compile_time_values, -) - -from prik.semantics import models as semantic_models - -from prik.semantics.native_contract import native_contract_issues - -from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text - -from prik.semantics.metadata import SCALAR_STORAGE_CATEGORY - - -from prik.codegen.printers import emit_module - from prik.semantics.models import ( - ProjectionMapping, - SemanticArgument, - SemanticField, - SemanticMethod, SemanticModule, SemanticClass, SemanticFunction, - SemanticConstraint, SemanticType, - SemanticVariable, ) @@ -88,55 +30,3 @@ def array_contract(semantic_type: SemanticType): assert semantic_type.storage is not None assert semantic_type.storage.array is not None return semantic_type.storage.array - - -__all__ = ( - "SCALAR_STORAGE_CATEGORY", - "FortranArgument", - "FortranBlockData", - "FortranDerivedType", - "FortranFile", - "FortranModule", - "FortranProcedureSignature", - "FortranProgram", - "FortranProject", - "FortranSubmodule", - "FortranToIRConverter", - "FortranUseMapping", - "FortranVariable", - "ProjectionMapping", - "SemanticArgument", - "SemanticClass", - "SemanticConstraint", - "SemanticField", - "SemanticFunction", - "SemanticMethod", - "SemanticModule", - "SemanticType", - "SemanticVariable", - "_compile_time_requirement_message", - "_iter_fortran_variable_contexts", - "_requirement_unit_name", - "_resolve_compile_time_text", - "array_contract", - "asdict", - "collect_fortran_type_storage_requirements", - "collect_semantic_compile_time_requirements", - "emit_module", - "fortran_file_to_semantic_modules", - "fortran_module_to_semantic_module", - "fortran_project_to_semantic_modules", - "fortran_type_storage_expression", - "get_class", - "get_function", - "has_constraint", - "json", - "native_contract_issues", - "parse_fortran_project", - "parse_fortran_source", - "parse_pyi_text", - "pytest", - "re", - "resolve_semantic_compile_time_values", - "semantic_models", -) diff --git a/tests/fortran/_support/semantic_properties.py b/tests/fortran/_support/semantic_properties.py index d43960b06..75686bb1c 100644 --- a/tests/fortran/_support/semantic_properties.py +++ b/tests/fortran/_support/semantic_properties.py @@ -2,34 +2,22 @@ from __future__ import annotations -from dataclasses import asdict import pytest pytest.importorskip("hypothesis") -from hypothesis import given, strategies as st +from hypothesis import strategies as st -from prik.semantics.fortran2ir import fortran_file_to_semantic_modules, resolve_semantic_compile_time_values from prik.semantics.models import ( - EXTERNAL_TYPE_REF_METADATA, OwnershipPolicy, - ProjectionMapping, - SemanticArgument, SemanticArrayContract, SemanticConstraint, - SemanticFunction, - SemanticModule, SemanticStorageContract, SemanticType, ) -from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text - -from prik.codegen.printers import emit_module - -from prik import parse_fortran_file _FORTRAN_SCALAR_TYPES = st.sampled_from( [ @@ -138,30 +126,3 @@ def canonical_semantic_types(draw): ownership=ownership, storage=storage, ) - - -__all__ = ( - "EXTERNAL_TYPE_REF_METADATA", - "_FORTRAN_VALUE_TYPES", - "_NATIVE_NAMES", - "_PYI_IDENTIFIER_STEMS", - "ProjectionMapping", - "SemanticArgument", - "SemanticArrayContract", - "SemanticConstraint", - "SemanticFunction", - "SemanticModule", - "SemanticStorageContract", - "SemanticType", - "asdict", - "canonical_semantic_types", - "emit_module", - "fortran_file_to_semantic_modules", - "fortran_scalar_subroutines", - "given", - "parse_fortran_file", - "parse_pyi_text", - "pytest", - "resolve_semantic_compile_time_values", - "st", -) diff --git a/tests/fortran/allocatables/parsing/test_allocatable_declarations.py b/tests/fortran/allocatables/parsing/test_allocatable_declarations.py index 7284b3c43..562d7bda1 100644 --- a/tests/fortran/allocatables/parsing/test_allocatable_declarations.py +++ b/tests/fortran/allocatables/parsing/test_allocatable_declarations.py @@ -1,8 +1,6 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( - parse_fortran_module, -) +from tests.fortran._support.parser_procedures import parse_fortran_module def test_module_allocatable_target_attribute_is_preserved(): diff --git a/tests/fortran/allocatables/pipeline/test_allocatable_contract_printing.py b/tests/fortran/allocatables/pipeline/test_allocatable_contract_printing.py index 44372d567..16ca692b3 100644 --- a/tests/fortran/allocatables/pipeline/test_allocatable_contract_printing.py +++ b/tests/fortran/allocatables/pipeline/test_allocatable_contract_printing.py @@ -1,10 +1,8 @@ """Generated contract surface for allocatable outputs and results.""" -from tests.fortran._support.semantic_conversion import ( - emit_module, - fortran_module_to_semantic_module, - parse_fortran_source, -) +from prik.codegen.printers import emit_module +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik import parse_fortran_file as parse_fortran_source def _generate_pyi(source: str) -> str: diff --git a/tests/fortran/allocatables/pipeline/test_allocatable_module_contract_printing.py b/tests/fortran/allocatables/pipeline/test_allocatable_module_contract_printing.py index 64613b397..d3b2bf369 100644 --- a/tests/fortran/allocatables/pipeline/test_allocatable_module_contract_printing.py +++ b/tests/fortran/allocatables/pipeline/test_allocatable_module_contract_printing.py @@ -1,11 +1,9 @@ """Generated and reparsed allocatable module and field declarations.""" -from tests.fortran._support.semantic_conversion import ( - emit_module, - fortran_module_to_semantic_module, - parse_fortran_source, - parse_pyi_text, -) +from prik.codegen.printers import emit_module +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik import parse_fortran_file as parse_fortran_source +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text def _generate_pyi(source: str) -> str: diff --git a/tests/fortran/allocatables/pipeline/test_allocatable_output_contract_printing.py b/tests/fortran/allocatables/pipeline/test_allocatable_output_contract_printing.py index d3ea085a2..c74098806 100644 --- a/tests/fortran/allocatables/pipeline/test_allocatable_output_contract_printing.py +++ b/tests/fortran/allocatables/pipeline/test_allocatable_output_contract_printing.py @@ -1,10 +1,8 @@ """Generated contract surface for optional allocatable outputs.""" -from tests.fortran._support.semantic_conversion import ( - emit_module, - fortran_module_to_semantic_module, - parse_fortran_source, -) +from prik.codegen.printers import emit_module +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik import parse_fortran_file as parse_fortran_source def _generate_pyi(source: str) -> str: diff --git a/tests/fortran/allocatables/policy/test_allocatable_handle_policy.py b/tests/fortran/allocatables/policy/test_allocatable_handle_policy.py index e727d4d54..05f9b78a6 100644 --- a/tests/fortran/allocatables/policy/test_allocatable_handle_policy.py +++ b/tests/fortran/allocatables/policy/test_allocatable_handle_policy.py @@ -1,20 +1,26 @@ """Tests split by stable ownership concept from `test_handle_policy_dispatch.py`.""" -from tests.fortran._support.ownership_policy import ( +from prik.semantics.models import ( + RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, + RESOLVED_OWNERSHIP_POLICY_METADATA, +) +from prik.semantics.native_array_handles import ( + NativeArrayBuildRequirement, + native_array_handle_build_requirements, +) +from prik.semantics.ownership import ( CodegenAction, DestructionPolicy, - NativeArrayBuildRequirement, NativeBarrierAction, OwnershipContext, OwnershipOwner, - RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, - RESOLVED_OWNERSHIP_POLICY_METADATA, StorageMode, TransferMode, - _array_type, - complete_semantic_policies, default_ownership_policy, - native_array_handle_build_requirements, +) +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.ownership_policy import ( + _array_type, parse_pyi_text, ) diff --git a/tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py b/tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py index 70fed39e3..fbb199ca8 100644 --- a/tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py +++ b/tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py @@ -1,12 +1,14 @@ """Allocatable handle handoff to ordinary native array arguments.""" -from tests.fortran._support.native_array_handles import ( +import numpy as np +import pytest +from prik.runtime.handles import ( AllocatableArray, + _native_array_actual_argument_for_binding_positional, +) +from tests.fortran._support.native_array_handles import ( _ArrayState, _handoff, - _native_array_actual_argument_for_binding_positional, - np, - pytest, ) diff --git a/tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py b/tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py index 90a13751c..77c093878 100644 --- a/tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py +++ b/tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py @@ -1,11 +1,9 @@ """Allocatable descriptor handoff through the runtime ABI.""" -from tests.fortran._support.native_array_handles import ( - AllocatableArray, - _handoff, - np, - pytest, -) +import numpy as np +import pytest +from prik.runtime.handles import AllocatableArray +from tests.fortran._support.native_array_handles import _handoff def test_allocatable_descriptor_hook_accepts_unallocated_descriptor_without_numpy_conversion(): diff --git a/tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py b/tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py index 7ecce6395..0b12b6abe 100644 --- a/tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py +++ b/tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py @@ -1,13 +1,15 @@ """Allocatable handle state, live-view, and operation protocol.""" -from tests.fortran._support.native_array_handles import ( +import numpy as np +import pytest +from prik.runtime.handles import ( AllocatableArray, NativeArrayHandleBase, +) +from tests.fortran._support.native_array_handles import ( _ArrayState, _common_ops, _required_handoff_ops, - np, - pytest, ) diff --git a/tests/fortran/allocatables/semantics/test_fortran_allocatable_semantics.py b/tests/fortran/allocatables/semantics/test_fortran_allocatable_semantics.py index ff566afa5..34337b4ef 100644 --- a/tests/fortran/allocatables/semantics/test_fortran_allocatable_semantics.py +++ b/tests/fortran/allocatables/semantics/test_fortran_allocatable_semantics.py @@ -1,12 +1,12 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( +from prik.semantics.fortran2ir import ( FortranToIRConverter, - ProjectionMapping, - array_contract, fortran_module_to_semantic_module, - parse_fortran_source, ) +from prik.semantics.models import ProjectionMapping +from tests.fortran._support.semantic_conversion import array_contract +from prik import parse_fortran_file as parse_fortran_source def test_converter_preserves_allocatable_target_metadata(): diff --git a/tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py b/tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py index e5e31ab09..8f86c4806 100644 --- a/tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py +++ b/tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py @@ -4,9 +4,9 @@ import pytest -from tests.fortran._support.semantic_conversion import parse_pyi_text from prik.semantics.metadata import PROJECTED_OUTPUT_METADATA from prik.semantics.native_array_handles import native_array_descriptor_kind +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text def test_persistent_allocatable_descriptors_preserve_scalar_and_array_kinds(): diff --git a/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py b/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py index 970232dfa..4ee517576 100644 --- a/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py +++ b/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py @@ -1,12 +1,14 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( +from prik import parse_fortran_file +from prik.parsers.fortran.models import ( FortranUseMapping, FortranVariable, +) +from tests.fortran._support.parser_procedures import ( collect_project_procedure_signatures, collect_signature_shape_symbols, evaluate_signature_shapes, - parse_fortran_file, ) diff --git a/tests/fortran/arrays/policy/test_array_shape_policy.py b/tests/fortran/arrays/policy/test_array_shape_policy.py index 65a6c1170..8ae19a28e 100644 --- a/tests/fortran/arrays/policy/test_array_shape_policy.py +++ b/tests/fortran/arrays/policy/test_array_shape_policy.py @@ -2,11 +2,8 @@ import pytest -from tests.fortran._support.semantic_conversion import ( - fortran_module_to_semantic_module, - get_function, - parse_fortran_source, -) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from tests.fortran._support.semantic_conversion import get_function from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.models import ( RESOLVED_DERIVED_TYPE_POLICY_METADATA, @@ -15,6 +12,7 @@ ) from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import DeclarationCallableAction +from prik import parse_fortran_file as parse_fortran_source def test_array_extent_reference_requires_a_visible_scalar_argument(): diff --git a/tests/fortran/arrays/semantics/test_array_semantics.py b/tests/fortran/arrays/semantics/test_array_semantics.py index 5c403389e..ca54a9d20 100644 --- a/tests/fortran/arrays/semantics/test_array_semantics.py +++ b/tests/fortran/arrays/semantics/test_array_semantics.py @@ -1,15 +1,17 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - array_contract, +from prik.semantics.fortran2ir import ( fortran_file_to_semantic_modules, fortran_module_to_semantic_module, +) +from tests.fortran._support.semantic_conversion import ( + array_contract, get_function, - parse_pyi_text, - parse_fortran_source, ) from prik.semantics.models import SemanticExpressionCallable from prik.codegen.printers import PyiPrinter +from prik import parse_fortran_file as parse_fortran_source +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text def test_array_constraints(): diff --git a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py index 104b0fdac..86ea56bb6 100644 --- a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py +++ b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py @@ -2,6 +2,10 @@ from __future__ import annotations +from pathlib import Path +import subprocess +import sys + import pytest from prik.utilities.declaration_expressions import ( @@ -319,3 +323,17 @@ def test_backend_renderer_rejects_invalid_target_and_unrenderable_syntax() -> No render_declaration_extent("not valid (", {}, target="c") with pytest.raises(ValueError, match="unsupported completed declaration-expression node"): render_declaration_extent("[n]", {}, target="c") + + +def test_declaration_expressions_direct_example_is_runnable() -> None: + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/utilities/declaration_expressions.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Public expression: source.shape[0]" in result.stdout + assert "Compile-time product: 6" in result.stdout diff --git a/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py b/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py index 302e2e295..7ec30b4a6 100644 --- a/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py +++ b/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py @@ -1,6 +1,7 @@ """Semantic callback prototype contract printing.""" -from tests.fortran._support.printer_models import ( +from prik.codegen.printers import emit_module +from prik.semantics.models import ( SemanticArgument, SemanticArrayContract, SemanticClass, @@ -9,7 +10,6 @@ SemanticPrototype, SemanticStorageContract, SemanticType, - emit_module, ) diff --git a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py index f313f29f7..4483530f1 100644 --- a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py @@ -1,14 +1,14 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - FortranToIRConverter, - emit_module, - get_function, - native_contract_issues, +from prik import ( parse_fortran_project, - parse_fortran_source, - parse_pyi_text, ) +from prik.codegen.printers import emit_module +from prik.semantics.fortran2ir import FortranToIRConverter +from prik.semantics.native_contract import native_contract_issues +from tests.fortran._support.semantic_conversion import get_function +from prik import parse_fortran_file as parse_fortran_source +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text def test_dummy_procedure_interfaces_become_complete_callable_contracts(): diff --git a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py index 38b95e435..47769df40 100644 --- a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py @@ -1,10 +1,8 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - complete_semantic_policies, - parse_pyi_text, - pytest, -) +import pytest +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_uses_value_default_and_explicit_reference_callbacks(): diff --git a/tests/fortran/command_line_interface/pipeline/_support.py b/tests/fortran/command_line_interface/pipeline/_support.py index e2a1e959a..9f06ea833 100644 --- a/tests/fortran/command_line_interface/pipeline/_support.py +++ b/tests/fortran/command_line_interface/pipeline/_support.py @@ -1,31 +1,8 @@ -import builtins - -from dataclasses import dataclass - -import json - -import os - -import runpy - -import subprocess - -import sys - import types - from pathlib import Path -import pytest - -from prik.parsers.fortran import cli as fortran_parser_cli - -from prik import FortranParseError - from prik import cli as prik_cli -from prik.pipeline.preprocessing import PreprocessingConfig, PreprocessingDiagnostic, PreprocessingError - TEST_FILE = Path(__file__).parents[2] / "source_parsing" / "parsing" / "fixtures" / "general" / "basic_subroutine.f90" @@ -131,28 +108,3 @@ def _patch_main_report_payloads( ), ) return preprocessing, calls - - -__all__ = ( - "TEST_FILE", - "FortranParseError", - "Path", - "PreprocessingConfig", - "PreprocessingDiagnostic", - "PreprocessingError", - "_MainParserError", - "_install_main_parser", - "_main_args", - "_patch_main_report_payloads", - "builtins", - "dataclass", - "fortran_parser_cli", - "json", - "os", - "prik_cli", - "pytest", - "runpy", - "subprocess", - "sys", - "types", -) diff --git a/tests/fortran/command_line_interface/pipeline/test_argument_contract.py b/tests/fortran/command_line_interface/pipeline/test_argument_contract.py index 21c71c2a6..ddf7aca79 100644 --- a/tests/fortran/command_line_interface/pipeline/test_argument_contract.py +++ b/tests/fortran/command_line_interface/pipeline/test_argument_contract.py @@ -1,18 +1,20 @@ """Tests split by stable CLI argument-contract ownership.""" +import json +from pathlib import Path +import subprocess +import sys +import types + +import pytest + +from prik import cli as prik_cli +from prik.pipeline.preprocessing import PreprocessingError from tests.fortran.command_line_interface.pipeline._support import ( - Path, - PreprocessingError, TEST_FILE, _MainParserError, _install_main_parser, _main_args, - json, - pytest, - subprocess, - sys, - types, - prik_cli, ) diff --git a/tests/fortran/command_line_interface/pipeline/test_output_contract.py b/tests/fortran/command_line_interface/pipeline/test_output_contract.py index 5967b5383..fd1877f7d 100644 --- a/tests/fortran/command_line_interface/pipeline/test_output_contract.py +++ b/tests/fortran/command_line_interface/pipeline/test_output_contract.py @@ -1,30 +1,32 @@ """Tests split by stable CLI output-contract ownership.""" +import builtins +from dataclasses import dataclass from importlib import metadata +import json +import os +from pathlib import Path import shutil +import subprocess +import sys +import types import prik +import pytest -from tests.fortran.command_line_interface.pipeline._support import ( - Path, +from prik import cli as prik_cli +from prik.parsers.fortran import cli as fortran_parser_cli +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingDiagnostic, PreprocessingError, +) +from tests.fortran.command_line_interface.pipeline._support import ( TEST_FILE, _MainParserError, _install_main_parser, _main_args, _patch_main_report_payloads, - builtins, - dataclass, - fortran_parser_cli, - json, - os, - pytest, - subprocess, - sys, - types, - prik_cli, ) diff --git a/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py b/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py index ce4db7e94..8d7d1e9d6 100644 --- a/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py +++ b/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py @@ -1,26 +1,30 @@ """Tests split by stable CLI stage-dispatch ownership.""" -from tests.fortran.command_line_interface.pipeline._support import ( - FortranParseError, - Path, +import json +import os +from pathlib import Path +import runpy +import subprocess +import sys +import types + +import pytest + +from prik import FortranParseError +from prik import cli as prik_cli +from prik.parsers.fortran import cli as fortran_parser_cli +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingDiagnostic, PreprocessingError, +) +from prik.semantics.fortran2ir import collect_semantic_compile_time_requirements +from tests.fortran.command_line_interface.pipeline._support import ( TEST_FILE, _install_main_parser, _main_args, _patch_main_report_payloads, - fortran_parser_cli, - json, - os, - pytest, - runpy, - subprocess, - sys, - types, - prik_cli, ) -from prik.semantics.fortran2ir import collect_semantic_compile_time_requirements def test_cli_keeps_free_procedure_when_module_has_same_name(tmp_path: Path): diff --git a/tests/fortran/data_types/parsing/test_declarations_and_shapes.py b/tests/fortran/data_types/parsing/test_declarations_and_shapes.py index a9f24bafd..73fafe3ef 100644 --- a/tests/fortran/data_types/parsing/test_declarations_and_shapes.py +++ b/tests/fortran/data_types/parsing/test_declarations_and_shapes.py @@ -4,13 +4,15 @@ import sys from pathlib import Path +import pytest +from prik import ( + parse_fortran_file, + parse_fortran_project, +) from tests.fortran._support.parser_procedures import ( COMPILE_TIME_EXPRESSION_SOURCE, collect_project_procedure_signatures, - parse_fortran_file, parse_fortran_modules, - parse_fortran_project, - pytest, ) from prik.parsers.fortran.parser import FortranParser diff --git a/tests/fortran/data_types/parsing/test_scalar_kind_parsing_properties.py b/tests/fortran/data_types/parsing/test_scalar_kind_parsing_properties.py index b8dd52a7c..990161271 100644 --- a/tests/fortran/data_types/parsing/test_scalar_kind_parsing_properties.py +++ b/tests/fortran/data_types/parsing/test_scalar_kind_parsing_properties.py @@ -1,12 +1,12 @@ """Generated Fortran intrinsic-kind parsing contracts.""" -from tests.fortran._support.parser_properties import ( - _FORTRAN_SCALAR_TYPES, +import pytest +from hypothesis import ( given, - parse_fortran_file, - pytest, - st, + strategies as st, ) +from prik import parse_fortran_file +from tests.fortran._support.parser_properties import _FORTRAN_SCALAR_TYPES @pytest.mark.property diff --git a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py index 12cc02d6e..de3493984 100644 --- a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py +++ b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py @@ -1,15 +1,17 @@ """Fortran scalar kinds, target storage facts, and semantic type mapping.""" -from tests.fortran._support.semantic_conversion import ( +import pytest +import re +from prik.parsers.fortran.models import ( FortranFile, - FortranToIRConverter, FortranVariable, +) +from prik.semantics.fortran2ir import ( + FortranToIRConverter, collect_fortran_type_storage_requirements, fortran_type_storage_expression, - parse_fortran_source, - pytest, - re, ) +from prik import parse_fortran_file as parse_fortran_source def test_intrinsic_builtin_kinds_map_to_semantic_types(): diff --git a/tests/fortran/data_types/semantics/test_types_and_storage.py b/tests/fortran/data_types/semantics/test_types_and_storage.py index a3dd080c9..c6128f438 100644 --- a/tests/fortran/data_types/semantics/test_types_and_storage.py +++ b/tests/fortran/data_types/semantics/test_types_and_storage.py @@ -1,23 +1,27 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( +import json +from dataclasses import asdict +from prik.parsers.fortran.models import ( FortranArgument, FortranDerivedType, FortranFile, FortranModule, FortranProcedureSignature, FortranProject, - FortranToIRConverter, FortranUseMapping, FortranVariable, - array_contract, - asdict, +) +from prik.semantics.fortran2ir import ( + FortranToIRConverter, fortran_module_to_semantic_module, +) +from tests.fortran._support.semantic_conversion import ( + array_contract, get_function, has_constraint, - json, - parse_fortran_source, ) +from prik import parse_fortran_file as parse_fortran_source def test_converter_visitor_and_compatibility_methods_cover_public_paths(): diff --git a/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py b/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py index b3f1a202f..8d3fe7337 100644 --- a/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py +++ b/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py @@ -1,8 +1,6 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( - parse_fortran_file, -) +from prik import parse_fortran_file def test_subroutine_derived_type_arguments_are_parsed(): diff --git a/tests/fortran/derived_types/parsing/test_derived_type_declarations.py b/tests/fortran/derived_types/parsing/test_derived_type_declarations.py index ad5c8d30a..c3da3443e 100644 --- a/tests/fortran/derived_types/parsing/test_derived_type_declarations.py +++ b/tests/fortran/derived_types/parsing/test_derived_type_declarations.py @@ -1,8 +1,6 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( - parse_fortran_file, -) +from prik import parse_fortran_file def test_derived_type_fields_and_methods_detection(): diff --git a/tests/fortran/derived_types/parsing/test_derived_type_properties.py b/tests/fortran/derived_types/parsing/test_derived_type_properties.py index b5400b72d..6eef82dc7 100644 --- a/tests/fortran/derived_types/parsing/test_derived_type_properties.py +++ b/tests/fortran/derived_types/parsing/test_derived_type_properties.py @@ -1,12 +1,14 @@ """Tests split by stable ownership concept from `test_properties.py`.""" +import pytest +from hypothesis import ( + given, + strategies as st, +) +from prik import parse_fortran_file from tests.fortran._support.parser_properties import ( _FORTRAN_IDENTIFIER_STEMS, _FORTRAN_SCALAR_TYPES, - given, - parse_fortran_file, - pytest, - st, ) diff --git a/tests/fortran/derived_types/parsing/test_parameterized_derived_types.py b/tests/fortran/derived_types/parsing/test_parameterized_derived_types.py index ea88065a6..aaa58ec45 100644 --- a/tests/fortran/derived_types/parsing/test_parameterized_derived_types.py +++ b/tests/fortran/derived_types/parsing/test_parameterized_derived_types.py @@ -1,9 +1,7 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( - COMPILE_TIME_EXPRESSION_SOURCE, - parse_fortran_file, -) +from prik import parse_fortran_file +from tests.fortran._support.parser_procedures import COMPILE_TIME_EXPRESSION_SOURCE def test_parameterized_derived_type_declarations_preserve_and_resolve_arguments(): diff --git a/tests/fortran/derived_types/parsing/test_type_bound_diagnostics.py b/tests/fortran/derived_types/parsing/test_type_bound_diagnostics.py index d5087964e..567025bde 100644 --- a/tests/fortran/derived_types/parsing/test_type_bound_diagnostics.py +++ b/tests/fortran/derived_types/parsing/test_type_bound_diagnostics.py @@ -1,11 +1,9 @@ """Tests split by stable ownership concept from `test_source_form_and_diagnostics_regressions.py`.""" -from tests.fortran._support.parser_regressions import ( - FortranDerivedType, - FortranParseError, - FortranParser, - pytest, -) +import pytest +from prik import FortranParseError +from prik.parsers.fortran.models import FortranDerivedType +from prik.parsers.fortran.parser import FortranParser def test_malformed_type_bound_declaration_diagnostic_preserves_public_metadata(): diff --git a/tests/fortran/derived_types/policy/test_derived_accessor_policy.py b/tests/fortran/derived_types/policy/test_derived_accessor_policy.py index d4c7b5223..86de0200f 100644 --- a/tests/fortran/derived_types/policy/test_derived_accessor_policy.py +++ b/tests/fortran/derived_types/policy/test_derived_accessor_policy.py @@ -1,11 +1,6 @@ """Tests split by stable ownership concept from `test_handle_policy_dispatch.py`.""" -from tests.fortran._support.ownership_policy import ( - AssignmentMode, - CodegenAction, - DestructionPolicy, - ObjectKind, - OwnershipOwner, +from prik.semantics.models import ( RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA, RESOLVED_OWNERSHIP_POLICY_METADATA, RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA, @@ -14,15 +9,24 @@ SemanticField, SemanticModule, SemanticVariable, +) +from prik.semantics.ownership import ( + AssignmentMode, + CodegenAction, + DestructionPolicy, + ObjectKind, + OwnershipOwner, SetterAction, StorageMode, TransferMode, + set_ownership_metadata, +) +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.ownership_policy import ( _array_type, _derived_type, _scalar_type, - complete_semantic_policies, parse_pyi_text, - set_ownership_metadata, ) from prik.semantics.models import ( diff --git a/tests/fortran/derived_types/policy/test_derived_policy_defaults.py b/tests/fortran/derived_types/policy/test_derived_policy_defaults.py index ab7e0a4a7..ecba1a345 100644 --- a/tests/fortran/derived_types/policy/test_derived_policy_defaults.py +++ b/tests/fortran/derived_types/policy/test_derived_policy_defaults.py @@ -1,22 +1,26 @@ """Tests split by stable ownership concept from `test_handle_policy_dispatch.py`.""" -from tests.fortran._support.ownership_policy import ( - CodegenAction, - DestructionPolicy, - OwnershipOwner, +from prik.semantics.models import ( SemanticArgument, SemanticClass, SemanticField, SemanticFunction, SemanticModule, SemanticVariable, +) +from prik.semantics.ownership import ( + CodegenAction, + DestructionPolicy, + OwnershipOwner, TransferMode, + default_ownership_policy, +) +from tests.fortran._support.ownership_policy import ( _array_type, _derived_type, _hidden_output_context, _scalar_type, _writable_argument_context, - default_ownership_policy, ) diff --git a/tests/fortran/derived_types/semantics/test_derived_type_identity.py b/tests/fortran/derived_types/semantics/test_derived_type_identity.py index e8bc64c79..d71f6b8aa 100644 --- a/tests/fortran/derived_types/semantics/test_derived_type_identity.py +++ b/tests/fortran/derived_types/semantics/test_derived_type_identity.py @@ -1,10 +1,8 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - fortran_module_to_semantic_module, - get_function, - parse_fortran_source, -) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from tests.fortran._support.semantic_conversion import get_function +from prik import parse_fortran_file as parse_fortran_source def test_procedure_local_derived_type_rename_uses_origin_type_identity(): diff --git a/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py b/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py index 2fdfe48ca..0568b8b52 100644 --- a/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py +++ b/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py @@ -1,25 +1,29 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( +import pytest +from prik.codegen.printers import emit_module +from prik.parsers.fortran.models import ( FortranArgument, FortranDerivedType, FortranFile, FortranProcedureSignature, - FortranToIRConverter, FortranVariable, +) +from prik.semantics.fortran2ir import ( + FortranToIRConverter, + fortran_module_to_semantic_module, +) +from prik.semantics.models import ( SemanticArgument, SemanticConstraint, SemanticFunction, SemanticMethod, SemanticType, - emit_module, - fortran_module_to_semantic_module, - get_class, - native_contract_issues, - parse_fortran_source, - parse_pyi_text, - pytest, ) +from prik.semantics.native_contract import native_contract_issues +from tests.fortran._support.semantic_conversion import get_class +from prik import parse_fortran_file as parse_fortran_source +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text def test_converter_rejects_unsupported_inputs_and_missing_derived_type_names(): diff --git a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py index 904057576..9c86449bb 100644 --- a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py +++ b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py @@ -1,25 +1,31 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( +from dataclasses import asdict +from prik import ( + parse_fortran_project, +) +from prik.parsers.fortran.models import ( FortranArgument, FortranDerivedType, FortranFile, FortranModule, FortranProcedureSignature, FortranProject, - FortranToIRConverter, FortranUseMapping, FortranVariable, - SemanticField, - SemanticVariable, - asdict, +) +from prik.semantics.fortran2ir import ( + FortranToIRConverter, fortran_file_to_semantic_modules, fortran_module_to_semantic_module, fortran_project_to_semantic_modules, - get_function, - parse_fortran_project, - parse_fortran_source, ) +from prik.semantics.models import ( + SemanticField, + SemanticVariable, +) +from tests.fortran._support.semantic_conversion import get_function +from prik import parse_fortran_file as parse_fortran_source def test_converter_preserves_imported_derived_contexts_through_dispatch_paths(): diff --git a/tests/fortran/derived_types/semantics/test_pyi_class_semantics.py b/tests/fortran/derived_types/semantics/test_pyi_class_semantics.py index b49f98e8d..b8014a4f7 100644 --- a/tests/fortran/derived_types/semantics/test_pyi_class_semantics.py +++ b/tests/fortran/derived_types/semantics/test_pyi_class_semantics.py @@ -1,11 +1,11 @@ """Core `.pyi` class semantics retained by Derived Types.""" -from tests.fortran._support.pyi_conversion import ( +from prik.codegen.printers import emit_module +from prik.semantics.metadata import ( PROJECTED_OUTPUT_METADATA, SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA, - emit_module, - parse_pyi_text, ) +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_self_only_generated_constructor_keeps_default_initializer(): diff --git a/tests/fortran/enumerations/parsing/test_enum_diagnostics.py b/tests/fortran/enumerations/parsing/test_enum_diagnostics.py index 8a9d64edd..4166b7e5d 100644 --- a/tests/fortran/enumerations/parsing/test_enum_diagnostics.py +++ b/tests/fortran/enumerations/parsing/test_enum_diagnostics.py @@ -3,7 +3,8 @@ import pytest from prik import FortranParseError, parse_fortran_file -from tests.fortran._support.parser_regressions import FortranParser, _unit +from prik.parsers.fortran.parser import FortranParser +from tests.fortran._support.parser_regressions import _unit def test_enum_diagnostic_reports_first_invalid_line_after_valid_enumerator(): diff --git a/tests/fortran/enumerations/semantics/test_enum_compile_time_values.py b/tests/fortran/enumerations/semantics/test_enum_compile_time_values.py index 304991e3b..c3730fc04 100644 --- a/tests/fortran/enumerations/semantics/test_enum_compile_time_values.py +++ b/tests/fortran/enumerations/semantics/test_enum_compile_time_values.py @@ -1,10 +1,10 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( +from prik.semantics.fortran2ir import resolve_semantic_compile_time_values +from prik.semantics.models import ( SemanticModule, SemanticType, SemanticVariable, - resolve_semantic_compile_time_values, ) diff --git a/tests/fortran/enumerations/semantics/test_pyi_enum_constants.py b/tests/fortran/enumerations/semantics/test_pyi_enum_constants.py index 0caf200b9..90e1f51a5 100644 --- a/tests/fortran/enumerations/semantics/test_pyi_enum_constants.py +++ b/tests/fortran/enumerations/semantics/test_pyi_enum_constants.py @@ -1,9 +1,7 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - emit_module, - parse_pyi_text, -) +from prik.codegen.printers import emit_module +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_round_trips_enum_like_integer_constants(): diff --git a/tests/fortran/enumerations/semantics/test_unsupported_enum_classes.py b/tests/fortran/enumerations/semantics/test_unsupported_enum_classes.py index 8a19e4a43..1e8989ff1 100644 --- a/tests/fortran/enumerations/semantics/test_unsupported_enum_classes.py +++ b/tests/fortran/enumerations/semantics/test_unsupported_enum_classes.py @@ -1,9 +1,7 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - parse_pyi_text, - pytest, -) +import pytest +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_rejects_enum_classes(): diff --git a/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py b/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py index 2c8279983..d0233186d 100644 --- a/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py +++ b/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py @@ -1,13 +1,15 @@ """Tests split by stable ownership concept from `test_source_form_and_diagnostics_regressions.py`.""" -from tests.fortran._support.parser_regressions import ( +from prik import parse_fortran_file +from prik.parsers.fortran.models import ( FortranArgument, - FortranParser, FortranProcedureSignature, +) +from prik.parsers.fortran.parser import ( + FortranParser, _ParserScope, - _unit, - parse_fortran_file, ) +from tests.fortran._support.parser_regressions import _unit def test_function_result_assignment_name_with_intrinsic_prefix_starts_execution_part(): diff --git a/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py b/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py index 52b7c0b72..301670ec5 100644 --- a/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py +++ b/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py @@ -1,19 +1,21 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - FortranProcedureSignature, +from prik.parsers.fortran.models import FortranProcedureSignature +from prik.semantics.fortran2ir import ( FortranToIRConverter, + fortran_file_to_semantic_modules, + fortran_module_to_semantic_module, +) +from prik.semantics.models import ( ProjectionMapping, SemanticArgument, SemanticFunction, SemanticMethod, SemanticType, - fortran_file_to_semantic_modules, - fortran_module_to_semantic_module, - get_function, - parse_fortran_source, - semantic_models, ) +from tests.fortran._support.semantic_conversion import get_function +from prik import parse_fortran_file as parse_fortran_source +from prik.semantics import models as semantic_models def test_bind_c_name_and_value_calling_convention_reach_semantic_ir(): diff --git a/tests/fortran/functions/semantics/test_fortran_function_semantics.py b/tests/fortran/functions/semantics/test_fortran_function_semantics.py index a846b5511..346ee408d 100644 --- a/tests/fortran/functions/semantics/test_fortran_function_semantics.py +++ b/tests/fortran/functions/semantics/test_fortran_function_semantics.py @@ -1,13 +1,13 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - ProjectionMapping, +from prik.semantics.fortran2ir import ( fortran_file_to_semantic_modules, fortran_module_to_semantic_module, - get_function, - parse_fortran_source, ) +from prik.semantics.models import ProjectionMapping +from tests.fortran._support.semantic_conversion import get_function from prik.semantics.metadata import PROJECTED_OUTPUT_METADATA +from prik import parse_fortran_file as parse_fortran_source def test_missing_intent_scalar_uses_conservative_replacement_projection(): diff --git a/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py b/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py index 758885a99..3d7c5a598 100644 --- a/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py +++ b/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py @@ -4,7 +4,11 @@ import pytest -from tests.fortran._support.parser_procedures import parse_fortran_file, parse_fortran_interfaces, parse_fortran_module +from prik import parse_fortran_file +from tests.fortran._support.parser_procedures import ( + parse_fortran_interfaces, + parse_fortran_module, +) from prik.parsers.fortran.models import FortranParseError FIXTURES = Path(__file__).parents[1] / "end_to_end" / "fixtures" diff --git a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py index 13b0f0635..09acbfbe6 100644 --- a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py @@ -2,13 +2,13 @@ from pathlib import Path -from tests.fortran._support.semantic_conversion import ( +import pytest +from prik.semantics.fortran2ir import ( FortranToIRConverter, fortran_module_to_semantic_module, - parse_fortran_source, - pytest, ) from prik.semantics.metadata import BIND_TARGET_METADATA +from prik import parse_fortran_file as parse_fortran_source OPERATOR_F90_SOURCE = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "foperators_f90.f90" diff --git a/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py b/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py index 270a41b4d..c610f0129 100644 --- a/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py @@ -1,6 +1,7 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import parse_pyi_text, pytest +import pytest +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_resolves_prik_overload_by_explicit_specific_name(): diff --git a/tests/fortran/infrastructure/codegen/test_native_binding_support.py b/tests/fortran/infrastructure/binding_support/test_native_support.py similarity index 86% rename from tests/fortran/infrastructure/codegen/test_native_binding_support.py rename to tests/fortran/infrastructure/binding_support/test_native_support.py index b9214f3cf..a05c92745 100644 --- a/tests/fortran/infrastructure/codegen/test_native_binding_support.py +++ b/tests/fortran/infrastructure/binding_support/test_native_support.py @@ -49,14 +49,3 @@ def test_native_binding_support_is_header_only_and_exposes_the_small_prik_api(): assert f"prik_{suffix}_to_python" in header assert f"prik_{suffix}_to_numpy" in header assert header.count("static inline") == 42 - - removed_compatibility_names = ( - "PyInt32_to_Int32", - "Double_to_PyDouble", - "Complex128_to_PyComplex", - "capsule_cleanup", - "pyarray_check", - "to_pyarray", - ) - for name in removed_compatibility_names: - assert name not in header diff --git a/tests/fortran/infrastructure/codegen/test_binding.py b/tests/fortran/infrastructure/codegen/test_binding.py new file mode 100644 index 000000000..522adffc6 --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_binding.py @@ -0,0 +1,39 @@ +"""Internal CPython binding lowering contracts.""" + +from __future__ import annotations + +from pathlib import Path +import subprocess +import sys + +import pytest + +from prik.codegen import ( + BackendScalarType, +) +from prik.codegen.c.binding import CBindingGenerator + + +def test_c_binding_rejects_an_unprefixed_numpy_scalar_macro(): + scalar = BackendScalarType( + semantic_name="Invalid", + c_spelling="double", + fortran_spelling="real(c_double)", + numpy_type_macro="FLOAT64", + ) + + with pytest.raises(ValueError, match="Unsupported NumPy scalar type macro 'FLOAT64'"): + CBindingGenerator()._scalar_helper_suffix(scalar) + + +def test_binding_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/c/binding.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Native procedure: DOUBLE_VALUE" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_bridge.py b/tests/fortran/infrastructure/codegen/test_bridge.py new file mode 100644 index 000000000..eddc692e2 --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_bridge.py @@ -0,0 +1,18 @@ +"""Internal Fortran bridge lowering contracts.""" + +from pathlib import Path +import subprocess +import sys + + +def test_bridge_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/fortran/bridge.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Bridge module: bind_c_bridge_demo_wrapper" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_codegen_package_boundaries.py b/tests/fortran/infrastructure/codegen/test_checks.py similarity index 71% rename from tests/fortran/infrastructure/codegen/test_codegen_package_boundaries.py rename to tests/fortran/infrastructure/codegen/test_checks.py index 738c3e2ab..426432232 100644 --- a/tests/fortran/infrastructure/codegen/test_codegen_package_boundaries.py +++ b/tests/fortran/infrastructure/codegen/test_checks.py @@ -1,15 +1,13 @@ -"""Boundary tests for the isolated wrapper-codegen package.""" +"""Internal code-generation static-check contracts.""" from __future__ import annotations import ast -from dataclasses import fields from pathlib import Path import subprocess import sys from tests.fortran._support.wrapper_build import REPO_ROOT -from prik.pipeline.wrapper_artifacts import GeneratedWrapperArtifacts from prik.codegen.checks import ( WrapperCodegenCheckConfig, check_codegen_package, @@ -51,27 +49,6 @@ def _check_source(tmp_path: Path, source: str, *, filename: str = "bad.py") -> s return {violation.code for violation in violations} -def test_canonical_printers_share_one_package(): - printers = CODEGEN_ROOT / "printers" - - assert (printers / "pyi_printer.py").is_file() - assert (printers / "source_printers.py").is_file() - - -def test_backend_generators_do_not_import_each_other(): - binding_imports = _imported_modules(CODEGEN_ROOT / "c" / "binding.py") - bridge_imports = _imported_modules(CODEGEN_ROOT / "fortran" / "bridge.py") - - assert not _imports_under(binding_imports, "prik.codegen.fortran") - assert not _imports_under(bridge_imports, "prik.codegen.c") - - -def test_wrapper_build_pipeline_imports_canonical_generator(): - imports = _imported_modules(SOURCE_ROOT / "pipeline" / "build.py") - - assert _imports_under(imports, "prik.codegen") - - def test_codegen_package_static_contracts_pass(): assert check_codegen_package(CODEGEN_ROOT) == () @@ -184,25 +161,3 @@ def _emit_item(self, node): ) assert "handler-printer-call" in codes - - -def test_generated_wrapper_artifacts_keep_compile_and_link_ownership_out_of_the_handoff(): - artifacts = GeneratedWrapperArtifacts( - module_name="demo", - bridge_sources=(Path("bind_c_demo.f90"),), - binding_sources=(Path("demo.c"),), - header_files=(Path("demo.h"),), - native_support_keys=("binding_support",), - ) - - assert artifacts.source_files == (Path("bind_c_demo.f90"), Path("demo.c")) - assert artifacts.generated_files == (Path("bind_c_demo.f90"), Path("demo.c"), Path("demo.h")) - assert artifacts.required_headers == () - assert {field.name for field in fields(GeneratedWrapperArtifacts)} == { - "module_name", - "bridge_sources", - "binding_sources", - "header_files", - "native_support_keys", - "required_headers", - } diff --git a/tests/fortran/infrastructure/codegen/test_docstrings.py b/tests/fortran/infrastructure/codegen/test_docstrings.py new file mode 100644 index 000000000..bb011d089 --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_docstrings.py @@ -0,0 +1,18 @@ +"""Internal generated-docstring contracts.""" + +from pathlib import Path +import subprocess +import sys + + +def test_docstrings_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/docstrings.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "double_value" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_generator.py b/tests/fortran/infrastructure/codegen/test_generator.py new file mode 100644 index 000000000..45a3cb87a --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_generator.py @@ -0,0 +1,532 @@ +"""Internal ordered wrapper-generation contracts.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path +import subprocess +import sys + +import pytest + +from tests.fortran._support.ownership_policy import parse_pyi_text +from prik.semantics.policy_completion import complete_semantic_policies +from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind +from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction +from prik.stage_values import FrozenStageRecordError +from prik.codegen import ( + CBindingGenerator, + CSourcePrinter, + FortranBridgeGenerator, + FortranSourcePrinter, + NamespacePlan, + WrapperCodeGenerator, + WrapperPlanner, +) + + +def _rendered_source(artifacts, suffix: str) -> str: + return next(source.text for source in artifacts.sources if source.path.name.endswith(suffix)) + + +def _plan(source: str, *, module_name: str = "fmath"): + module = parse_pyi_text(source, module_name=module_name) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def _scalar_plan(): + return _plan( + """ +@nogil +@bind("SWAP_ARGS") +@standalone +@native_call([Addr(Arg(1)), Addr(Arg(0))]) +def swap_args(x: Float64, y: Float64) -> Float64: ... +""", + module_name="runtime_policy", + ) + + +def _hidden_result_plan(): + return _plan( + """ +@native_call([Int32(1), Arg(0), Bool(False), Return("result", 0)]) +def scale(x: Float64) -> Float64: ... +""", + module_name="hidden_values", + ) + + +def _edit_first_function(plan, edit): + root = plan.namespaces[0] + functions = (edit(root.functions[0]), *root.functions[1:]) + return replace(plan, namespaces=(replace(root, functions=functions), *plan.namespaces[1:])) + + +def _scalar_boundary_plan(): + module = parse_pyi_text( + """ +def storage(x: Float64[()]) -> None: ... +def raw(x: Addr(Float64)) -> None: ... +def direct_storage_result() -> Float64[()]: ... +@native_call([Return("out", 0)]) +def hidden_storage_result() -> Float64[()]: ... +""", + module_name="scalar_boundaries", + ) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def test_public_generator_directly_returns_complete_rendered_artifacts(): + plan = _plan( + """ +@nogil +@bind("SWAP_ARGS") +@standalone +@native_call([Addr(Arg(1)), Addr(Arg(0))]) +def swap_args(x: Float64, y: Float64) -> Float64: ... +""", + module_name="render_demo", + ) + + artifacts = WrapperCodeGenerator().generate(plan) + c_source = _rendered_source(artifacts, ".c") + c_header = _rendered_source(artifacts, ".h") + fortran_source = _rendered_source(artifacts, ".f90") + + assert artifacts.artifacts.module_name == "render_demo" + assert artifacts.source_paths == ( + Path("bind_c_render_demo_wrapper.f90"), + Path("render_demo_wrapper.c"), + Path("render_demo_wrapper.h"), + ) + assert artifacts.extension_init_name == "PyInit_render_demo" + assert "double bind_c_swap_args(double * y, double * x);" in c_source + assert 'static char * kwlist[] = {"x", "y", NULL};' in c_source + assert 'PyArg_ParseTupleAndKeywords(args, kwargs, "OO", kwlist, &bound_x_obj, &bound_y_obj)' in c_source + assert "prik_float64_unpack_exact(bound_x_obj, &bound_x)" in c_source + assert "result = bind_c_swap_args(&bound_y, &bound_x);" in c_source + assert "PyObject * result_obj = prik_float64_to_python(&result);" in c_source + assert "PyMODINIT_FUNC PyInit_render_demo(void)" in c_source + assert "static PyObject * wrap_swap_args" in c_header + assert "module bind_c_render_demo_wrapper" in fortran_source + assert 'function bind_c_swap_args(y, x) result(result) bind(c, name="bind_c_swap_args")' in fortran_source + assert "real(c_double), external :: SWAP_ARGS" in fortran_source + assert "function SWAP_ARGS(" not in fortran_source + assert "result = SWAP_ARGS(y, x)" in fortran_source + + +def test_public_generator_reports_each_rendering_operation_in_execution_order(): + plan = _plan("def value(x: Float64) -> Float64: ...", module_name="render_progress") + progress = [] + + WrapperCodeGenerator().generate(plan, progress=lambda label, elapsed: progress.append((label, elapsed))) + + assert [label for label, _ in progress] == [ + "Generate binding source", + "Generate binding source", + "Generate bridge source", + "Generate bridge source", + "Generate binding header", + "Generate binding header", + ] + assert [elapsed is None for _, elapsed in progress] == [True, False, True, False, True, False] + assert all(elapsed >= 0.0 for _, elapsed in progress if elapsed is not None) + + +def test_large_procedure_only_binding_is_split_into_balanced_compile_units(): + declarations = "\n".join(f"def value_{index:03d}(x: Float64) -> Float64: ..." for index in range(128)) + artifacts = WrapperCodeGenerator().generate(_plan(declarations, module_name="large_binding")) + binding_sources = [source for source in artifacts.sources if source.path.name.startswith("large_binding_wrapper")] + main_source, *worker_sources, header_source = binding_sources + + assert artifacts.artifacts.binding_sources == ( + Path("large_binding_wrapper.c"), + Path("large_binding_wrapper_001.c"), + Path("large_binding_wrapper_002.c"), + Path("large_binding_wrapper_003.c"), + Path("large_binding_wrapper_004.c"), + ) + assert "#define PRIK_BINDING_IMPORT_ARRAY 1" in main_source.text + assert "PyMODINIT_FUNC PyInit_large_binding(void)" in main_source.text + assert "PyObject * wrap_value_000(" not in main_source.text + assert all("PRIK_BINDING_IMPORT_ARRAY" not in source.text for source in worker_sources) + assert all("PyInit_large_binding" not in source.text for source in worker_sources) + assert sum(source.text.count("PyObject * wrap_value_") for source in worker_sources) == 128 + assert "static PyObject * wrap_value_000" not in header_source.text + assert "PyObject * wrap_value_000(PyObject * self, PyObject * args, PyObject * kwargs);" in header_source.text + + +def test_procedure_only_binding_below_sharding_threshold_keeps_one_compile_unit(): + declarations = "\n".join(f"def value_{index:03d}(x: Float64) -> Float64: ..." for index in range(127)) + + artifacts = WrapperCodeGenerator().generate(_plan(declarations, module_name="unsharded_binding")) + + assert artifacts.artifacts.binding_sources == (Path("unsharded_binding_wrapper.c"),) + + +@pytest.mark.parametrize( + ("source", "c_fragment", "fortran_fragment"), + [ + ( + "def required_value(x: Float64) -> Float64: ...", + "PyObject * bound_x_obj;", + "result = native_required_value(x)", + ), + ( + "def optional_value(x: Int32 = ...) -> Int32: ...", + "PyObject * bound_x_obj = Py_None;", + "if (c_associated(bound_x)) then", + ), + ( + """ +@native_call([Allocatable(Arg(0))]) +def descriptor_value(value: Annotated[Float64, Immutable] | None = ...) -> Int32: ... +""", + "PyObject * bound_value_obj = NULL;", + "type(c_ptr), value :: bound_value_present", + ), + ( + """ +@native_call([Addr(Arg(0)), Return("result", 0)]) +def hidden_value(x: Float64) -> Float64: ... +""", + "void bind_c_hidden_value(double * x, double * result);", + "subroutine bind_c_hidden_value(x, result)", + ), + ], +) +def test_supported_function_actions_select_their_backend_behavior(source, c_fragment, fortran_fragment): + artifacts = WrapperCodeGenerator().generate(_plan(source, module_name="action_dispatch")) + + assert c_fragment in _rendered_source(artifacts, ".c") + assert fortran_fragment in _rendered_source(artifacts, ".f90") + + +def test_direct_plan_edits_change_binding_and_bridge_generation_then_freeze_plan(): + plan = _plan( + """ +@bind("ADD_R8") +@standalone +@native_call([Addr(Arg(0)), Addr(Arg(1))]) +def calculate(x: Float64, y: Float64) -> Float64: ... +""", + module_name="editable_plan", + ) + function = plan.namespaces[0].functions[0] + function.binding.python_name = "subtract" + function.owner_path = "editable_plan.subtract" + function.bridge.native_name = "SUB_R8" + + artifacts = WrapperCodeGenerator().generate(plan) + + assert '"subtract", (PyCFunction)wrap_calculate' in _rendered_source(artifacts, ".c") + assert "result = SUB_R8(x, y)" in _rendered_source(artifacts, ".f90") + with pytest.raises(FrozenStageRecordError): + function.bridge.native_name = "ADD_R8" + + +def test_backend_visitors_return_complete_nodes_and_printers_freeze_them(): + plan = _plan( + """ +@bind("SCALE") +@native_call([Int32(1), Arg(0), Bool(False)]) +def scale(x: Float64) -> Float64: ... +""", + module_name="backend_nodes", + ) + c_generator = CBindingGenerator() + fortran_generator = FortranBridgeGenerator() + c_generator.require_supported(plan) + fortran_generator.require_supported(plan) + + c_module, c_header = c_generator.visit(plan) + fortran_module = fortran_generator.visit(plan) + + assert [function.name for function in c_module.functions] == ["wrap_scale", "PyInit_backend_nodes"] + assert [prototype.name for prototype in c_header.prototypes] == ["wrap_scale"] + assert [procedure.name for procedure in fortran_module.procedures] == ["bind_c_scale"] + assert "result = native_scale(1, x, .false.)" in FortranSourcePrinter().doprint(fortran_module) + CSourcePrinter().doprint(c_module) + with pytest.raises(FrozenStageRecordError): + c_module.name = "later" + with pytest.raises(FrozenStageRecordError): + fortran_module.name = "later" + + +def test_generator_rejects_unregistered_typed_lowering_combination(): + plan = _plan( + """ +def scale(x: Float64) -> Float64: ... +""", + module_name="unsupported_lowering", + ) + function = plan.namespaces[0].functions[0] + invalid_argument = replace( + function.arguments[0], + binding=replace(function.arguments[0].binding, optional_mode="x"), + bridge=replace(function.arguments[0].bridge, optional_mode="x"), + ) + root = plan.namespaces[0] + invalid = replace( + plan, + namespaces=(replace(root, functions=(replace(function, arguments=(invalid_argument,)),)),), + ) + + with pytest.raises(ValueError, match="Unsupported C argument optional mode"): + WrapperCodeGenerator().generate(invalid) + + +def test_generator_rejects_hidden_result_native_action_disagreement(): + plan = _hidden_result_plan() + function = plan.namespaces[0].functions[0] + result = function.results[0] + replacement = ( + NativeBarrierAction.PASS_VALUE + if result.bridge.native_action is not NativeBarrierAction.PASS_VALUE + else NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS + ) + invalid = _edit_first_function( + plan, + lambda item: replace( + item, + results=(replace(result, bridge=replace(result.bridge, native_action=replacement)),), + ), + ) + + with pytest.raises(ValueError, match="inconsistent-result-native-action"): + WrapperCodeGenerator().generate(invalid) + + +def test_generator_rejects_hidden_result_slot_codegen_action_disagreement(): + plan = _hidden_result_plan() + function = plan.namespaces[0].functions[0] + result = function.results[0] + edited_slot = replace(result.native_call_slot, codegen_action=CodegenAction.COPY_OUT) + invalid = _edit_first_function( + plan, + lambda item: replace( + item, + results=(replace(result, native_call_slot=edited_slot),), + native_call_slots=tuple( + edited_slot if slot.native_position == edited_slot.native_position else slot + for slot in item.native_call_slots + ), + ), + ) + + with pytest.raises(ValueError, match="inconsistent-result-slot-codegen-action"): + WrapperCodeGenerator().generate(invalid) + + +def test_generator_rejects_argument_native_slot_object_kind_disagreement(): + plan = _scalar_plan() + argument = plan.namespaces[0].functions[0].arguments[0] + argument.native_call_slot.object_kind = ObjectKind.STRING + + with pytest.raises(ValueError, match="inconsistent-argument-object-kind"): + WrapperCodeGenerator().generate(plan) + + +def test_generator_rejects_result_native_slot_object_kind_disagreement(): + plan = _hidden_result_plan() + result = plan.namespaces[0].functions[0].results[0] + result.native_call_slot.object_kind = ObjectKind.STRING + + with pytest.raises(ValueError, match="inconsistent-result-object-kind"): + WrapperCodeGenerator().generate(plan) + + +def test_generator_rejects_advertised_role_without_a_plan_producer(): + invalid = _edit_first_function( + _scalar_plan(), + lambda function: replace(function, available_roles=(*function.available_roles, "invented:role")), + ) + + with pytest.raises(ValueError, match="inconsistent-available-roles"): + WrapperCodeGenerator().generate(invalid) + + +def test_generator_rejects_duplicate_python_exports_before_lowering(): + plan = _scalar_plan() + root = plan.namespaces[0] + function = root.functions[0] + duplicate = replace(function, symbol_name="other_symbol") + invalid = replace(plan, namespaces=(replace(root, functions=(function, duplicate)),)) + + with pytest.raises(ValueError, match="duplicate-python-export"): + WrapperCodeGenerator().generate(invalid) + + +def test_generator_rejects_duplicate_generated_symbols_before_lowering(): + plan = _scalar_plan() + root = plan.namespaces[0] + function = root.functions[0] + duplicate = replace( + function, + owner_path="runtime_policy.other", + binding=replace(function.binding, python_name="other"), + ) + invalid = replace(plan, namespaces=(replace(root, functions=(function, duplicate)),)) + + with pytest.raises(ValueError, match="duplicate-generated-symbol"): + WrapperCodeGenerator().generate(invalid) + + +def test_generator_rejects_colliding_generated_namespace_symbols(): + plan = _scalar_plan() + invalid = replace( + plan, + namespaces=( + *plan.namespaces, + NamespacePlan(owner_path="runtime_policy.root", python_path=("root",)), + ), + ) + + with pytest.raises(ValueError, match="duplicate-generated-namespace-symbol"): + WrapperCodeGenerator().generate(invalid) + + +@pytest.mark.parametrize( + ("mutate", "expected_code"), + [ + ( + lambda plan: replace( + plan, + binding=replace(plan.binding, owner_path="other"), + ), + "binding-module-owner", + ), + ( + lambda plan: _edit_first_function( + plan, + lambda function: replace( + function, + arguments=( + replace(function.arguments[0], python_position=99), + function.arguments[1], + ), + ), + ), + "out-of-range-python-position", + ), + ( + lambda plan: _edit_first_function( + plan, + lambda function: replace( + function, + arguments=( + replace( + function.arguments[0], + bridge=replace( + function.arguments[0].bridge, + handoff_role="other:role", + ), + ), + function.arguments[1], + ), + ), + ), + "inconsistent-bridge-handoff", + ), + ], +) +def test_generator_revalidates_direct_plan_edits(mutate, expected_code): + invalid = mutate(_scalar_plan()) + + with pytest.raises(ValueError, match=expected_code): + WrapperCodeGenerator().generate(invalid) + + +@pytest.mark.parametrize( + ("edit", "diagnostic"), + [ + ("native_action", "invalid-scalar-storage-native-action"), + ("handoff", "invalid-scalar-storage-handoff-mode"), + ("data_action", "invalid-scalar-storage-data-action"), + ("codegen", "invalid-scalar-storage-codegen-action"), + ("array", "invalid-scalar-storage-array"), + ], +) +def test_scalar_address_handoff_plan_edits_fail_before_lowering(edit, diagnostic): + plan = _scalar_boundary_plan() + storage = plan.namespaces[0].functions[0].arguments[0] + if edit == "native_action": + storage.bridge.native_action = NativeBarrierAction.PASS_VALUE + elif edit == "handoff": + storage.bridge.handoff_mode = ArgumentHandoffMode.VALUE + elif edit == "data_action": + storage.bridge.data_action = BridgeDataAction.COPY_REPRESENTATION + storage.bridge.copy_reason = "edited scalar-storage copy" + storage.native_call_slot.bridge_data_action = BridgeDataAction.COPY_REPRESENTATION + storage.native_call_slot.bridge_copy_reason = "edited scalar-storage copy" + elif edit == "codegen": + storage.binding.codegen_action = CodegenAction.SNAPSHOT_COPY + else: + storage.array.rank = 1 + + with pytest.raises(ValueError, match=diagnostic): + WrapperCodeGenerator().generate(plan) + + +@pytest.mark.parametrize( + ("action", "reason", "diagnostic"), + [ + (BridgeDataAction.COPY_REPRESENTATION, None, "missing-bridge-copy-reason"), + (BridgeDataAction.ASSOCIATE_VIEW, "unnecessary second copy", "unexpected-bridge-copy-reason"), + (BridgeDataAction.BLOCKED, None, "blocked-bridge-data-action"), + ], +) +def test_bridge_data_action_invariant_rejects_unjustified_or_blocked_plans(action, reason, diagnostic): + plan = _scalar_boundary_plan() + function = plan.namespaces[0].functions[0] + storage = function.arguments[0] + storage.bridge.data_action = action + storage.bridge.copy_reason = reason + storage.native_call_slot.bridge_data_action = action + storage.native_call_slot.bridge_copy_reason = reason + assert function.native_call_slots[storage.native_position] is storage.native_call_slot + + with pytest.raises(ValueError, match=diagnostic): + WrapperCodeGenerator().generate(plan) + + +def test_scalar_copy_in_out_reuses_one_binding_local_without_bridge_copy(): + module = parse_pyi_text( + 'def bump(value: Annotated[Int32, Immutable]) -> Returns["value", Int32]: ...', + module_name="one_copy", + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + value = plan.namespaces[0].functions[0].arguments[0] + assert value.bridge.data_action is BridgeDataAction.DIRECT_TRANSFER + assert value.bridge.copy_reason is None + + artifacts = WrapperCodeGenerator().generate(plan) + c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") + bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") + + assert c_source.count("int32_t bound_value;") == 1 + assert "prik_int32_unpack_exact(bound_value_obj, &bound_value)" in c_source + assert "bind_c_bump(&bound_value);" in c_source + assert "PyObject * result_obj = NULL;" in c_source + assert "result_obj = prik_int32_to_python(&bound_value);" in c_source + assert "integer(c_int32_t) :: value" in bridge_source + assert "call native_bump(value)" in bridge_source + assert "value =" not in bridge_source + assert "value_input" not in bridge_source + + +def test_generator_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/generator.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Extension initializer: PyInit_generator_demo" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_package.py b/tests/fortran/infrastructure/codegen/test_package.py new file mode 100644 index 000000000..42b2517f1 --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_package.py @@ -0,0 +1,67 @@ +"""Internal code-generation package boundary contracts.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +from tests.fortran._support.wrapper_build import REPO_ROOT +from prik.codegen.checks import ( + WrapperCodegenCheckConfig, + check_codegen_paths, +) + +SOURCE_ROOT = REPO_ROOT / "prik" +CODEGEN_ROOT = SOURCE_ROOT / "codegen" + + +def _imported_modules(path: Path) -> set[str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + imported = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imported.update(alias.name for alias in node.names) + if isinstance(node, ast.ImportFrom) and node.module: + imported.add(node.module) + return imported + + +def _imports_under(imports: set[str], package: str) -> bool: + return any(name == package or name.startswith(f"{package}.") for name in imports) + + +def _write_module(root: Path, relative_path: str, source: str) -> Path: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source, encoding="utf-8") + return path + + +def _check_source(tmp_path: Path, source: str, *, filename: str = "bad.py") -> set[str]: + path = _write_module(tmp_path, filename, source) + violations = check_codegen_paths( + [path], + config=WrapperCodegenCheckConfig(max_complexity=3, max_statements=4, max_nesting=2), + ) + return {violation.code for violation in violations} + + +def test_canonical_printers_share_one_package(): + printers = CODEGEN_ROOT / "printers" + + assert (printers / "pyi_printer.py").is_file() + assert (printers / "source_printers.py").is_file() + + +def test_backend_generators_do_not_import_each_other(): + binding_imports = _imported_modules(CODEGEN_ROOT / "c" / "binding.py") + bridge_imports = _imported_modules(CODEGEN_ROOT / "fortran" / "bridge.py") + + assert not _imports_under(binding_imports, "prik.codegen.fortran") + assert not _imports_under(bridge_imports, "prik.codegen.c") + + +def test_wrapper_build_pipeline_imports_canonical_generator(): + imports = _imported_modules(SOURCE_ROOT / "pipeline" / "build.py") + + assert _imports_under(imports, "prik.codegen") diff --git a/tests/fortran/infrastructure/codegen/test_plan.py b/tests/fortran/infrastructure/codegen/test_plan.py new file mode 100644 index 000000000..cf1c25d4d --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_plan.py @@ -0,0 +1,18 @@ +"""Internal typed wrapper-plan contracts.""" + +from pathlib import Path +import subprocess +import sys + + +def test_plan_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/plan.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Plan owner: demo" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_wrapper_plan_validation.py b/tests/fortran/infrastructure/codegen/test_planner.py similarity index 50% rename from tests/fortran/infrastructure/codegen/test_wrapper_plan_validation.py rename to tests/fortran/infrastructure/codegen/test_planner.py index 16b12f88c..03a40f8c1 100644 --- a/tests/fortran/infrastructure/codegen/test_wrapper_plan_validation.py +++ b/tests/fortran/infrastructure/codegen/test_planner.py @@ -1,17 +1,19 @@ -"""Cross-feature wrapper-plan projection and structural validation tests.""" +"""Internal semantic-policy to wrapper-plan projection contracts.""" from __future__ import annotations +from pathlib import Path +import subprocess +import sys + from dataclasses import replace import pytest from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.models import PYTHON_EXPORTS_METADATA -from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind from prik.semantics.policy_completion import complete_semantic_policies from prik.codegen import ( - NamespacePlan, WrapperCodeGenerator, WrapperPlanner, ) @@ -52,76 +54,6 @@ def _edit_first_function(plan, edit): return replace(plan, namespaces=(replace(root, functions=functions), *plan.namespaces[1:])) -def test_generator_rejects_hidden_result_native_action_disagreement(): - plan = _hidden_result_plan() - function = plan.namespaces[0].functions[0] - result = function.results[0] - replacement = ( - NativeBarrierAction.PASS_VALUE - if result.bridge.native_action is not NativeBarrierAction.PASS_VALUE - else NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS - ) - invalid = _edit_first_function( - plan, - lambda item: replace( - item, - results=(replace(result, bridge=replace(result.bridge, native_action=replacement)),), - ), - ) - - with pytest.raises(ValueError, match="inconsistent-result-native-action"): - WrapperCodeGenerator().generate(invalid) - - -def test_generator_rejects_hidden_result_slot_codegen_action_disagreement(): - plan = _hidden_result_plan() - function = plan.namespaces[0].functions[0] - result = function.results[0] - edited_slot = replace(result.native_call_slot, codegen_action=CodegenAction.COPY_OUT) - invalid = _edit_first_function( - plan, - lambda item: replace( - item, - results=(replace(result, native_call_slot=edited_slot),), - native_call_slots=tuple( - edited_slot if slot.native_position == edited_slot.native_position else slot - for slot in item.native_call_slots - ), - ), - ) - - with pytest.raises(ValueError, match="inconsistent-result-slot-codegen-action"): - WrapperCodeGenerator().generate(invalid) - - -def test_generator_rejects_argument_native_slot_object_kind_disagreement(): - plan = _scalar_plan() - argument = plan.namespaces[0].functions[0].arguments[0] - argument.native_call_slot.object_kind = ObjectKind.STRING - - with pytest.raises(ValueError, match="inconsistent-argument-object-kind"): - WrapperCodeGenerator().generate(plan) - - -def test_generator_rejects_result_native_slot_object_kind_disagreement(): - plan = _hidden_result_plan() - result = plan.namespaces[0].functions[0].results[0] - result.native_call_slot.object_kind = ObjectKind.STRING - - with pytest.raises(ValueError, match="inconsistent-result-object-kind"): - WrapperCodeGenerator().generate(plan) - - -def test_generator_rejects_advertised_role_without_a_plan_producer(): - invalid = _edit_first_function( - _scalar_plan(), - lambda function: replace(function, available_roles=(*function.available_roles, "invented:role")), - ) - - with pytest.raises(ValueError, match="inconsistent-available-roles"): - WrapperCodeGenerator().generate(invalid) - - def test_planner_groups_completed_exports_into_explicit_namespace_nodes(): module = parse_pyi_text( """ @@ -236,17 +168,6 @@ def test_planner_rejects_a_module_without_public_wrapper_exports(): WrapperPlanner().build(module) -def test_completed_function_policy_rejects_unimplemented_runtime_constraints(): - module = parse_pyi_text( - "def solve(value: Annotated[Int32, Bounded(1, 8), Finite]) -> Int32: ...\n", - module_name="constrained", - ) - complete_semantic_policies(module) - - with pytest.raises(ValueError, match="no runtime validators for semantic constraints"): - WrapperPlanner().build(module) - - def test_planner_reports_an_unsupported_completed_module_policy_at_its_owner_path(): module = parse_pyi_text( """ @@ -263,92 +184,14 @@ def test_planner_reports_an_unsupported_completed_module_policy_at_its_owner_pat WrapperPlanner().build(module) -def test_generator_rejects_duplicate_python_exports_before_lowering(): - plan = _scalar_plan() - root = plan.namespaces[0] - function = root.functions[0] - duplicate = replace(function, symbol_name="other_symbol") - invalid = replace(plan, namespaces=(replace(root, functions=(function, duplicate)),)) - - with pytest.raises(ValueError, match="duplicate-python-export"): - WrapperCodeGenerator().generate(invalid) - - -def test_generator_rejects_duplicate_generated_symbols_before_lowering(): - plan = _scalar_plan() - root = plan.namespaces[0] - function = root.functions[0] - duplicate = replace( - function, - owner_path="runtime_policy.other", - binding=replace(function.binding, python_name="other"), +def test_planner_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/planner.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, ) - invalid = replace(plan, namespaces=(replace(root, functions=(function, duplicate)),)) - - with pytest.raises(ValueError, match="duplicate-generated-symbol"): - WrapperCodeGenerator().generate(invalid) - - -def test_generator_rejects_colliding_generated_namespace_symbols(): - plan = _scalar_plan() - invalid = replace( - plan, - namespaces=( - *plan.namespaces, - NamespacePlan(owner_path="runtime_policy.root", python_path=("root",)), - ), - ) - - with pytest.raises(ValueError, match="duplicate-generated-namespace-symbol"): - WrapperCodeGenerator().generate(invalid) - - -@pytest.mark.parametrize( - ("mutate", "expected_code"), - [ - ( - lambda plan: replace( - plan, - binding=replace(plan.binding, owner_path="other"), - ), - "binding-module-owner", - ), - ( - lambda plan: _edit_first_function( - plan, - lambda function: replace( - function, - arguments=( - replace(function.arguments[0], python_position=99), - function.arguments[1], - ), - ), - ), - "out-of-range-python-position", - ), - ( - lambda plan: _edit_first_function( - plan, - lambda function: replace( - function, - arguments=( - replace( - function.arguments[0], - bridge=replace( - function.arguments[0].bridge, - handoff_role="other:role", - ), - ), - function.arguments[1], - ), - ), - ), - "inconsistent-bridge-handoff", - ), - ], -) -def test_generator_revalidates_direct_plan_edits(mutate, expected_code): - invalid = mutate(_scalar_plan()) - with pytest.raises(ValueError, match=expected_code): - WrapperCodeGenerator().generate(invalid) + assert "Native target: DOUBLE_VALUE" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_pyi_printer.py b/tests/fortran/infrastructure/codegen/test_pyi_printer.py new file mode 100644 index 000000000..f57ee6b9e --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_pyi_printer.py @@ -0,0 +1,18 @@ +"""Internal semantic pyi printer contracts.""" + +from pathlib import Path +import subprocess +import sys + + +def test_pyi_printer_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/printers/pyi_printer.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Semantic module: printer_demo" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py b/tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py deleted file mode 100644 index b40c6823d..000000000 --- a/tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Cross-feature direct-plan scalar boundary validation.""" - -from __future__ import annotations - -import pytest - -from tests.fortran._support.ownership_policy import parse_pyi_text -from prik.semantics.ownership import CodegenAction, NativeBarrierAction -from prik.semantics.policy_completion import complete_semantic_policies -from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction -from prik.codegen import WrapperCodeGenerator, WrapperPlanner - - -def _scalar_boundary_plan(): - module = parse_pyi_text( - """ -def storage(x: Float64[()]) -> None: ... -def raw(x: Addr(Float64)) -> None: ... -def direct_storage_result() -> Float64[()]: ... -@native_call([Return("out", 0)]) -def hidden_storage_result() -> Float64[()]: ... -""", - module_name="scalar_boundaries", - ) - complete_semantic_policies(module) - return WrapperPlanner().build(module) - - -@pytest.mark.parametrize( - ("edit", "diagnostic"), - [ - ("native_action", "invalid-scalar-storage-native-action"), - ("handoff", "invalid-scalar-storage-handoff-mode"), - ("data_action", "invalid-scalar-storage-data-action"), - ("codegen", "invalid-scalar-storage-codegen-action"), - ("array", "invalid-scalar-storage-array"), - ], -) -def test_scalar_address_handoff_plan_edits_fail_before_lowering(edit, diagnostic): - plan = _scalar_boundary_plan() - storage = plan.namespaces[0].functions[0].arguments[0] - if edit == "native_action": - storage.bridge.native_action = NativeBarrierAction.PASS_VALUE - elif edit == "handoff": - storage.bridge.handoff_mode = ArgumentHandoffMode.VALUE - elif edit == "data_action": - storage.bridge.data_action = BridgeDataAction.COPY_REPRESENTATION - storage.bridge.copy_reason = "edited scalar-storage copy" - storage.native_call_slot.bridge_data_action = BridgeDataAction.COPY_REPRESENTATION - storage.native_call_slot.bridge_copy_reason = "edited scalar-storage copy" - elif edit == "codegen": - storage.binding.codegen_action = CodegenAction.SNAPSHOT_COPY - else: - storage.array.rank = 1 - - with pytest.raises(ValueError, match=diagnostic): - WrapperCodeGenerator().generate(plan) - - -@pytest.mark.parametrize( - ("action", "reason", "diagnostic"), - [ - (BridgeDataAction.COPY_REPRESENTATION, None, "missing-bridge-copy-reason"), - (BridgeDataAction.ASSOCIATE_VIEW, "unnecessary second copy", "unexpected-bridge-copy-reason"), - (BridgeDataAction.BLOCKED, None, "blocked-bridge-data-action"), - ], -) -def test_bridge_data_action_invariant_rejects_unjustified_or_blocked_plans(action, reason, diagnostic): - plan = _scalar_boundary_plan() - function = plan.namespaces[0].functions[0] - storage = function.arguments[0] - storage.bridge.data_action = action - storage.bridge.copy_reason = reason - storage.native_call_slot.bridge_data_action = action - storage.native_call_slot.bridge_copy_reason = reason - assert function.native_call_slots[storage.native_position] is storage.native_call_slot - - with pytest.raises(ValueError, match=diagnostic): - WrapperCodeGenerator().generate(plan) - - -def test_scalar_copy_in_out_reuses_one_binding_local_without_bridge_copy(): - module = parse_pyi_text( - 'def bump(value: Annotated[Int32, Immutable]) -> Returns["value", Int32]: ...', - module_name="one_copy", - ) - complete_semantic_policies(module) - plan = WrapperPlanner().build(module) - value = plan.namespaces[0].functions[0].arguments[0] - assert value.bridge.data_action is BridgeDataAction.DIRECT_TRANSFER - assert value.bridge.copy_reason is None - - artifacts = WrapperCodeGenerator().generate(plan) - c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") - bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") - - assert c_source.count("int32_t bound_value;") == 1 - assert "prik_int32_unpack_exact(bound_value_obj, &bound_value)" in c_source - assert "bind_c_bump(&bound_value);" in c_source - assert "PyObject * result_obj = NULL;" in c_source - assert "result_obj = prik_int32_to_python(&bound_value);" in c_source - assert "integer(c_int32_t) :: value" in bridge_source - assert "call native_bump(value)" in bridge_source - assert "value =" not in bridge_source - assert "value_input" not in bridge_source diff --git a/tests/fortran/infrastructure/codegen/test_backend_foundations.py b/tests/fortran/infrastructure/codegen/test_source_printers.py similarity index 93% rename from tests/fortran/infrastructure/codegen/test_backend_foundations.py rename to tests/fortran/infrastructure/codegen/test_source_printers.py index a8ad12da5..8b54fa0e9 100644 --- a/tests/fortran/infrastructure/codegen/test_backend_foundations.py +++ b/tests/fortran/infrastructure/codegen/test_source_printers.py @@ -1,10 +1,13 @@ -"""Tests for isolated primitive backend foundations.""" +"""Internal C and Fortran source-printer contracts.""" from __future__ import annotations +from pathlib import Path +import subprocess +import sys + import ast import re -from pathlib import Path import pytest @@ -37,7 +40,6 @@ NamespacePlan, UnsupportedWrapperCodegenNodeError, ) -from prik.codegen.c.binding import CBindingGenerator def test_source_printers_render_complete_c_header_and_fortran_modules(): @@ -117,18 +119,6 @@ def test_source_printers_reject_wrapper_plan_models(): FortranSourcePrinter().doprint(plan) -def test_c_binding_rejects_an_unprefixed_numpy_scalar_macro(): - scalar = BackendScalarType( - semantic_name="Invalid", - c_spelling="double", - fortran_spelling="real(c_double)", - numpy_type_macro="FLOAT64", - ) - - with pytest.raises(ValueError, match="Unsupported NumPy scalar type macro 'FLOAT64'"): - CBindingGenerator()._scalar_helper_suffix(scalar) - - def test_fortran_source_printer_wraps_long_parenthesized_call_arguments(): slices = ", ".join(f"1:values_upper_bound_{axis} + 1:values_stride_{axis}" for axis in range(4)) source = FortranSourcePrinter().doprint( @@ -229,3 +219,16 @@ def test_source_printers_do_not_import_wrapper_plan_models(): } assert "prik.codegen.plan" not in imports + + +def test_source_printers_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + result = subprocess.run( + [sys.executable, str(repository_root / "prik/codegen/printers/source_printers.py")], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert "Rendered Fortran bridge source:" in result.stdout diff --git a/tests/fortran/infrastructure/codegen/test_wrapper_assembly.py b/tests/fortran/infrastructure/codegen/test_wrapper_assembly.py deleted file mode 100644 index 03d2d0c99..000000000 --- a/tests/fortran/infrastructure/codegen/test_wrapper_assembly.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Cross-feature direct generation and editable-plan assembly tests.""" - -from __future__ import annotations - -from dataclasses import replace -from pathlib import Path - -import pytest - -from tests.fortran._support.ownership_policy import parse_pyi_text -from prik.semantics.policy_completion import complete_semantic_policies -from prik.stage_values import FrozenStageRecordError -from prik.codegen import ( - CBindingGenerator, - CSourcePrinter, - FortranBridgeGenerator, - FortranSourcePrinter, - WrapperCodeGenerator, - WrapperPlanner, -) - - -def _plan(source: str, *, module_name: str): - module = parse_pyi_text(source, module_name=module_name) - complete_semantic_policies(module) - return WrapperPlanner().build(module) - - -def _rendered_source(artifacts, suffix: str) -> str: - return next(source.text for source in artifacts.sources if source.path.name.endswith(suffix)) - - -def test_public_generator_directly_returns_complete_rendered_artifacts(): - plan = _plan( - """ -@nogil -@bind("SWAP_ARGS") -@standalone -@native_call([Addr(Arg(1)), Addr(Arg(0))]) -def swap_args(x: Float64, y: Float64) -> Float64: ... -""", - module_name="render_demo", - ) - - artifacts = WrapperCodeGenerator().generate(plan) - c_source = _rendered_source(artifacts, ".c") - c_header = _rendered_source(artifacts, ".h") - fortran_source = _rendered_source(artifacts, ".f90") - - assert artifacts.artifacts.module_name == "render_demo" - assert artifacts.source_paths == ( - Path("bind_c_render_demo_wrapper.f90"), - Path("render_demo_wrapper.c"), - Path("render_demo_wrapper.h"), - ) - assert artifacts.extension_init_name == "PyInit_render_demo" - assert "double bind_c_swap_args(double * y, double * x);" in c_source - assert 'static char * kwlist[] = {"x", "y", NULL};' in c_source - assert 'PyArg_ParseTupleAndKeywords(args, kwargs, "OO", kwlist, &bound_x_obj, &bound_y_obj)' in c_source - assert "prik_float64_unpack_exact(bound_x_obj, &bound_x)" in c_source - assert "result = bind_c_swap_args(&bound_y, &bound_x);" in c_source - assert "PyObject * result_obj = prik_float64_to_python(&result);" in c_source - assert "PyMODINIT_FUNC PyInit_render_demo(void)" in c_source - assert "static PyObject * wrap_swap_args" in c_header - assert "module bind_c_render_demo_wrapper" in fortran_source - assert 'function bind_c_swap_args(y, x) result(result) bind(c, name="bind_c_swap_args")' in fortran_source - assert "real(c_double), external :: SWAP_ARGS" in fortran_source - assert "function SWAP_ARGS(" not in fortran_source - assert "result = SWAP_ARGS(y, x)" in fortran_source - - -def test_public_generator_reports_each_rendering_operation_in_execution_order(): - plan = _plan("def value(x: Float64) -> Float64: ...", module_name="render_progress") - progress = [] - - WrapperCodeGenerator().generate(plan, progress=lambda label, elapsed: progress.append((label, elapsed))) - - assert [label for label, _ in progress] == [ - "Generate binding source", - "Generate binding source", - "Generate bridge source", - "Generate bridge source", - "Generate binding header", - "Generate binding header", - ] - assert [elapsed is None for _, elapsed in progress] == [True, False, True, False, True, False] - assert all(elapsed >= 0.0 for _, elapsed in progress if elapsed is not None) - - -def test_large_procedure_only_binding_is_split_into_balanced_compile_units(): - declarations = "\n".join(f"def value_{index:03d}(x: Float64) -> Float64: ..." for index in range(128)) - artifacts = WrapperCodeGenerator().generate(_plan(declarations, module_name="large_binding")) - binding_sources = [source for source in artifacts.sources if source.path.name.startswith("large_binding_wrapper")] - main_source, *worker_sources, header_source = binding_sources - - assert artifacts.artifacts.binding_sources == ( - Path("large_binding_wrapper.c"), - Path("large_binding_wrapper_001.c"), - Path("large_binding_wrapper_002.c"), - Path("large_binding_wrapper_003.c"), - Path("large_binding_wrapper_004.c"), - ) - assert "#define PRIK_BINDING_IMPORT_ARRAY 1" in main_source.text - assert "PyMODINIT_FUNC PyInit_large_binding(void)" in main_source.text - assert "PyObject * wrap_value_000(" not in main_source.text - assert all("PRIK_BINDING_IMPORT_ARRAY" not in source.text for source in worker_sources) - assert all("PyInit_large_binding" not in source.text for source in worker_sources) - assert sum(source.text.count("PyObject * wrap_value_") for source in worker_sources) == 128 - assert "static PyObject * wrap_value_000" not in header_source.text - assert "PyObject * wrap_value_000(PyObject * self, PyObject * args, PyObject * kwargs);" in header_source.text - - -def test_procedure_only_binding_below_sharding_threshold_keeps_one_compile_unit(): - declarations = "\n".join(f"def value_{index:03d}(x: Float64) -> Float64: ..." for index in range(127)) - - artifacts = WrapperCodeGenerator().generate(_plan(declarations, module_name="unsharded_binding")) - - assert artifacts.artifacts.binding_sources == (Path("unsharded_binding_wrapper.c"),) - - -@pytest.mark.parametrize( - ("source", "c_fragment", "fortran_fragment"), - [ - ( - "def required_value(x: Float64) -> Float64: ...", - "PyObject * bound_x_obj;", - "result = native_required_value(x)", - ), - ( - "def optional_value(x: Int32 = ...) -> Int32: ...", - "PyObject * bound_x_obj = Py_None;", - "if (c_associated(bound_x)) then", - ), - ( - """ -@native_call([Allocatable(Arg(0))]) -def descriptor_value(value: Annotated[Float64, Immutable] | None = ...) -> Int32: ... -""", - "PyObject * bound_value_obj = NULL;", - "type(c_ptr), value :: bound_value_present", - ), - ( - """ -@native_call([Addr(Arg(0)), Return("result", 0)]) -def hidden_value(x: Float64) -> Float64: ... -""", - "void bind_c_hidden_value(double * x, double * result);", - "subroutine bind_c_hidden_value(x, result)", - ), - ], -) -def test_supported_function_actions_select_their_backend_behavior(source, c_fragment, fortran_fragment): - artifacts = WrapperCodeGenerator().generate(_plan(source, module_name="action_dispatch")) - - assert c_fragment in _rendered_source(artifacts, ".c") - assert fortran_fragment in _rendered_source(artifacts, ".f90") - - -def test_direct_plan_edits_change_binding_and_bridge_generation_then_freeze_plan(): - plan = _plan( - """ -@bind("ADD_R8") -@standalone -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def calculate(x: Float64, y: Float64) -> Float64: ... -""", - module_name="editable_plan", - ) - function = plan.namespaces[0].functions[0] - function.binding.python_name = "subtract" - function.owner_path = "editable_plan.subtract" - function.bridge.native_name = "SUB_R8" - - artifacts = WrapperCodeGenerator().generate(plan) - - assert '"subtract", (PyCFunction)wrap_calculate' in _rendered_source(artifacts, ".c") - assert "result = SUB_R8(x, y)" in _rendered_source(artifacts, ".f90") - with pytest.raises(FrozenStageRecordError): - function.bridge.native_name = "ADD_R8" - - -def test_backend_visitors_return_complete_nodes_and_printers_freeze_them(): - plan = _plan( - """ -@bind("SCALE") -@native_call([Int32(1), Arg(0), Bool(False)]) -def scale(x: Float64) -> Float64: ... -""", - module_name="backend_nodes", - ) - c_generator = CBindingGenerator() - fortran_generator = FortranBridgeGenerator() - c_generator.require_supported(plan) - fortran_generator.require_supported(plan) - - c_module, c_header = c_generator.visit(plan) - fortran_module = fortran_generator.visit(plan) - - assert [function.name for function in c_module.functions] == ["wrap_scale", "PyInit_backend_nodes"] - assert [prototype.name for prototype in c_header.prototypes] == ["wrap_scale"] - assert [procedure.name for procedure in fortran_module.procedures] == ["bind_c_scale"] - assert "result = native_scale(1, x, .false.)" in FortranSourcePrinter().doprint(fortran_module) - CSourcePrinter().doprint(c_module) - with pytest.raises(FrozenStageRecordError): - c_module.name = "later" - with pytest.raises(FrozenStageRecordError): - fortran_module.name = "later" - - -def test_generator_rejects_unregistered_typed_lowering_combination(): - plan = _plan( - """ -def scale(x: Float64) -> Float64: ... -""", - module_name="unsupported_lowering", - ) - function = plan.namespaces[0].functions[0] - invalid_argument = replace( - function.arguments[0], - binding=replace(function.arguments[0].binding, optional_mode="x"), - bridge=replace(function.arguments[0].bridge, optional_mode="x"), - ) - root = plan.namespaces[0] - invalid = replace( - plan, - namespaces=(replace(root, functions=(replace(function, arguments=(invalid_argument,)),)),), - ) - - with pytest.raises(ValueError, match="Unsupported C argument optional mode"): - WrapperCodeGenerator().generate(invalid) diff --git a/tests/shared/naming/test_policy.py b/tests/fortran/infrastructure/naming/test_policy.py similarity index 100% rename from tests/shared/naming/test_policy.py rename to tests/fortran/infrastructure/naming/test_policy.py diff --git a/tests/fortran/infrastructure/pipeline/test_wrapper_artifacts.py b/tests/fortran/infrastructure/pipeline/test_wrapper_artifacts.py new file mode 100644 index 000000000..f50a8ef6b --- /dev/null +++ b/tests/fortran/infrastructure/pipeline/test_wrapper_artifacts.py @@ -0,0 +1,70 @@ +"""Internal rendered-wrapper artifact handoff contracts.""" + +from __future__ import annotations + +import ast +from dataclasses import fields +from pathlib import Path + +from tests.fortran._support.wrapper_build import REPO_ROOT +from prik.pipeline.wrapper_artifacts import GeneratedWrapperArtifacts +from prik.codegen.checks import ( + WrapperCodegenCheckConfig, + check_codegen_paths, +) + +SOURCE_ROOT = REPO_ROOT / "prik" +CODEGEN_ROOT = SOURCE_ROOT / "codegen" + + +def _imported_modules(path: Path) -> set[str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + imported = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imported.update(alias.name for alias in node.names) + if isinstance(node, ast.ImportFrom) and node.module: + imported.add(node.module) + return imported + + +def _imports_under(imports: set[str], package: str) -> bool: + return any(name == package or name.startswith(f"{package}.") for name in imports) + + +def _write_module(root: Path, relative_path: str, source: str) -> Path: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source, encoding="utf-8") + return path + + +def _check_source(tmp_path: Path, source: str, *, filename: str = "bad.py") -> set[str]: + path = _write_module(tmp_path, filename, source) + violations = check_codegen_paths( + [path], + config=WrapperCodegenCheckConfig(max_complexity=3, max_statements=4, max_nesting=2), + ) + return {violation.code for violation in violations} + + +def test_generated_wrapper_artifacts_keep_compile_and_link_ownership_out_of_the_handoff(): + artifacts = GeneratedWrapperArtifacts( + module_name="demo", + bridge_sources=(Path("bind_c_demo.f90"),), + binding_sources=(Path("demo.c"),), + header_files=(Path("demo.h"),), + native_support_keys=("binding_support",), + ) + + assert artifacts.source_files == (Path("bind_c_demo.f90"), Path("demo.c")) + assert artifacts.generated_files == (Path("bind_c_demo.f90"), Path("demo.c"), Path("demo.h")) + assert artifacts.required_headers == () + assert {field.name for field in fields(GeneratedWrapperArtifacts)} == { + "module_name", + "bridge_sources", + "binding_sources", + "header_files", + "native_support_keys", + "required_headers", + } diff --git a/tests/fortran/infrastructure/semantics/test_native_array_handles.py b/tests/fortran/infrastructure/semantics/test_native_array_handles.py new file mode 100644 index 000000000..f655e0080 --- /dev/null +++ b/tests/fortran/infrastructure/semantics/test_native_array_handles.py @@ -0,0 +1,45 @@ +"""Internal native-array handle policy dispatch contracts.""" + +from prik.semantics.native_array_handles import ( + ArrayInteropPolicy, + ArrayInteropPolicyDispatcher, +) + + +def test_array_interop_dispatcher_routes_completed_abi_selector_to_named_method(): + class Subject: + name = "values" + + class Target: + def data_buffer(self, subject, policy, marker): + return marker, subject.name, policy.abi + + def descriptor(self, subject, policy, marker): + return marker, subject.name, policy.abi, policy.descriptor_kind + + dispatcher = ArrayInteropPolicyDispatcher( + { + ("argument", "data_buffer"): "data_buffer", + ("argument", "descriptor"): "descriptor", + }, + ) + + assert dispatcher.dispatch( + Target(), + Subject(), + ArrayInteropPolicy(abi="data_buffer", owner="argument values"), + "argument", + "seen", + ) == ("seen", "values", "data_buffer") + assert dispatcher.dispatch( + Target(), + Subject(), + ArrayInteropPolicy( + abi="descriptor", + owner="argument values", + descriptor_kind="allocatable", + handle_kind="argument_descriptor", + ), + "argument", + "seen", + ) == ("seen", "values", "descriptor", "allocatable") diff --git a/tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py b/tests/fortran/infrastructure/semantics/test_ownership.py similarity index 73% rename from tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py rename to tests/fortran/infrastructure/semantics/test_ownership.py index d313dd5be..b964c1f4b 100644 --- a/tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py +++ b/tests/fortran/infrastructure/semantics/test_ownership.py @@ -1,13 +1,12 @@ -"""Tests split by stable ownership concept from `test_handle_policy_dispatch.py`.""" +"""Internal ownership defaults, dispatch, and validation contracts.""" from pathlib import Path import subprocess import sys -from tests.fortran._support.ownership_policy import ( - ADDRESS_ROLE_PROJECTION, - ArrayInteropPolicy, - ArrayInteropPolicyDispatcher, +import pytest +from prik.semantics.metadata import ADDRESS_ROLE_PROJECTION +from prik.semantics.ownership import ( CodegenAction, DestructionPolicy, NativeBarrierAction, @@ -17,25 +16,14 @@ OwnershipDecision, OwnershipOwner, OwnershipPolicyResolver, - POLICY_COMPLETION_PREPARED_METADATA, - PROJECTED_OUTPUT_METADATA, PolicyActionDispatcher, - ProjectionMapping, PythonBarrierAction, PythonBarrierDispatcher, - RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA, - RESOLVED_OWNERSHIP_POLICY_METADATA, - RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA, - RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA, - SemanticArgument, - SemanticClass, - SemanticField, - SemanticFunction, - SemanticModule, - SemanticVariable, - SetterAction, StorageMode, TransferMode, + default_ownership_policy, +) +from tests.fortran._support.ownership_policy import ( _address_type, _array_type, _derived_type, @@ -46,9 +34,6 @@ _string_storage_type, _string_type, _writable_argument_context, - complete_semantic_policies, - default_ownership_policy, - pytest, ) @@ -343,122 +328,6 @@ def test_barrier_dispatchers_reject_missing_completed_actions(): NativeBarrierDispatcher({}).handler_name_for_decision(decision, "x") -def test_array_interop_dispatcher_routes_completed_abi_selector_to_named_method(): - class Subject: - name = "values" - - class Target: - def data_buffer(self, subject, policy, marker): - return marker, subject.name, policy.abi - - def descriptor(self, subject, policy, marker): - return marker, subject.name, policy.abi, policy.descriptor_kind - - dispatcher = ArrayInteropPolicyDispatcher( - { - ("argument", "data_buffer"): "data_buffer", - ("argument", "descriptor"): "descriptor", - }, - ) - - assert dispatcher.dispatch( - Target(), - Subject(), - ArrayInteropPolicy(abi="data_buffer", owner="argument values"), - "argument", - "seen", - ) == ("seen", "values", "data_buffer") - assert dispatcher.dispatch( - Target(), - Subject(), - ArrayInteropPolicy( - abi="descriptor", - owner="argument values", - descriptor_kind="allocatable", - handle_kind="argument_descriptor", - ), - "argument", - "seen", - ) == ("seen", "values", "descriptor", "allocatable") - - -def test_policy_completion_attaches_decisions_before_ir_lowering(): - module = SemanticModule( - name="generated_policy", - variables=[ - SemanticVariable( - "module_values", - _array_type(allocatable=True, metadata={"aliased": True}), - ) - ], - classes=[ - SemanticClass( - "buffer", - fields=[SemanticField("values", _array_type(allocatable=True))], - ) - ], - functions=[ - SemanticFunction( - "replace", - arguments=[ - SemanticArgument( - "values", - _array_type(allocatable=True), - metadata={PROJECTED_OUTPUT_METADATA: True}, - ) - ], - return_type=None, - projection=[ - ProjectionMapping( - python_name="values", - native_name="values", - native_position=0, - python_position=0, - result_position=0, - ) - ], - ) - ], - ) - - complete_semantic_policies(module) - - assert module.metadata[POLICY_COMPLETION_PREPARED_METADATA] is True - assert module.variables[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].owner is OwnershipOwner.NATIVE - module_setter = module.variables[0].metadata[RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA] - assert module_setter.codegen_action is CodegenAction.CALL_LOCAL_INPUT - assert module_setter.setter_action is SetterAction.REJECT_REPLACEMENT - assert ( - module.variables[0].metadata[RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA].codegen_action - is CodegenAction.BORROWED_VIEW - ) - assert module.classes[0].fields[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].owner is OwnershipOwner.WRAPPER - assert ( - module.classes[0].fields[0].metadata[RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA].codegen_action - is CodegenAction.CALL_LOCAL_INPUT - ) - assert ( - module.functions[0].arguments[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].transfer is TransferMode.IN_PLACE - ) - assert RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA not in module.functions[0].metadata - - -def test_policy_completion_direct_example_is_runnable(): - repository_root = Path(__file__).resolve().parents[4] - - result = subprocess.run( - [sys.executable, "prik/semantics/policy_completion.py"], - cwd=repository_root, - capture_output=True, - check=True, - text=True, - ) - - assert result.stdout == ( - "before: math.scale(value): Float64 semantic IR\nafter: math.scale(value): scalar_value -> pass_value\n" - ) - - def test_ownership_policy_direct_example_is_runnable(): repository_root = Path(__file__).resolve().parents[4] diff --git a/tests/fortran/infrastructure/policy/test_accessor_and_storage_policy.py b/tests/fortran/infrastructure/semantics/test_policy_completion.py similarity index 67% rename from tests/fortran/infrastructure/policy/test_accessor_and_storage_policy.py rename to tests/fortran/infrastructure/semantics/test_policy_completion.py index 1ce863262..3d10a43ad 100644 --- a/tests/fortran/infrastructure/policy/test_accessor_and_storage_policy.py +++ b/tests/fortran/infrastructure/semantics/test_policy_completion.py @@ -1,12 +1,11 @@ -"""Tests split by stable ownership concept from `test_handle_policy_dispatch.py`.""" +"""Internal semantic-policy completion contracts.""" -from tests.fortran._support.ownership_policy import ( +import pytest +from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, ADDRESS_ROLE_PROJECTION, - AssignmentMode, - CodegenAction, - NativeBarrierAction, - PythonBarrierAction, +) +from prik.semantics.models import ( RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA, RESOLVED_OWNERSHIP_POLICY_METADATA, RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA, @@ -18,20 +17,38 @@ SemanticModule, SemanticType, SemanticVariable, +) +from prik.semantics.native_array_handles import native_array_descriptor_kind +from prik.semantics.ownership import ( + AssignmentMode, + CodegenAction, + NativeBarrierAction, + PythonBarrierAction, SetterAction, StorageMode, TransferMode, +) +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.ownership_policy import ( _scalar_type, - complete_semantic_policies, - native_array_descriptor_kind, parse_pyi_text, - pytest, ) from prik.semantics.models import ( RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA, SemanticCoercion, ) +from pathlib import Path +import subprocess +import sys + +from prik.semantics.metadata import PROJECTED_OUTPUT_METADATA +from prik.semantics.models import ( + POLICY_COMPLETION_PREPARED_METADATA, + ProjectionMapping, +) +from prik.semantics.ownership import OwnershipOwner +from tests.fortran._support.ownership_policy import _array_type def test_policy_completion_converts_native_addr_projection_after_python_boundary_parsing(): @@ -197,3 +214,80 @@ def test_semantic_coercion_without_conversion_action_blocks_completed_function_p policy = function.metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] assert policy.supported is False assert "argument 'value' has no wrapper conversion actions for semantic coercions ('Float64',)" in policy.blockers + + +def test_policy_completion_attaches_decisions_before_ir_lowering(): + module = SemanticModule( + name="generated_policy", + variables=[ + SemanticVariable( + "module_values", + _array_type(allocatable=True, metadata={"aliased": True}), + ) + ], + classes=[ + SemanticClass( + "buffer", + fields=[SemanticField("values", _array_type(allocatable=True))], + ) + ], + functions=[ + SemanticFunction( + "replace", + arguments=[ + SemanticArgument( + "values", + _array_type(allocatable=True), + metadata={PROJECTED_OUTPUT_METADATA: True}, + ) + ], + return_type=None, + projection=[ + ProjectionMapping( + python_name="values", + native_name="values", + native_position=0, + python_position=0, + result_position=0, + ) + ], + ) + ], + ) + + complete_semantic_policies(module) + + assert module.metadata[POLICY_COMPLETION_PREPARED_METADATA] is True + assert module.variables[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].owner is OwnershipOwner.NATIVE + module_setter = module.variables[0].metadata[RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA] + assert module_setter.codegen_action is CodegenAction.CALL_LOCAL_INPUT + assert module_setter.setter_action is SetterAction.REJECT_REPLACEMENT + assert ( + module.variables[0].metadata[RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA].codegen_action + is CodegenAction.BORROWED_VIEW + ) + assert module.classes[0].fields[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].owner is OwnershipOwner.WRAPPER + assert ( + module.classes[0].fields[0].metadata[RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA].codegen_action + is CodegenAction.CALL_LOCAL_INPUT + ) + assert ( + module.functions[0].arguments[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].transfer is TransferMode.IN_PLACE + ) + assert RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA not in module.functions[0].metadata + + +def test_policy_completion_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + + result = subprocess.run( + [sys.executable, "prik/semantics/policy_completion.py"], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert result.stdout == ( + "before: math.scale(value): Float64 semantic IR\nafter: math.scale(value): scalar_value -> pass_value\n" + ) diff --git a/tests/fortran/infrastructure/policy/test_wrapper_policy.py b/tests/fortran/infrastructure/semantics/test_wrapper_policy.py similarity index 98% rename from tests/fortran/infrastructure/policy/test_wrapper_policy.py rename to tests/fortran/infrastructure/semantics/test_wrapper_policy.py index c51920bc3..bf7d7ae83 100644 --- a/tests/fortran/infrastructure/policy/test_wrapper_policy.py +++ b/tests/fortran/infrastructure/semantics/test_wrapper_policy.py @@ -6,6 +6,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from tests.fortran._support.wrapper_build import wrapper_source +from prik.codegen import WrapperPlanner from prik.parsers.fortran.parser import parse_fortran_project from prik.pipeline.build import _apply_source_python_exports, _fortran_source_for_pipeline, _merge_wrapper_modules from prik.pipeline.preprocessing import PreprocessingConfig @@ -597,3 +598,14 @@ def test_wrapper_policy_direct_example_is_runnable(): "before: math.scale(value): Float64 semantic IR\n" "after: direct_transfer; result=native_scalar; native=pass_value\n" ) + + +def test_completed_function_policy_rejects_unimplemented_runtime_constraints(): + module = parse_pyi_text( + "def solve(value: Annotated[Int32, Bounded(1, 8), Finite]) -> Int32: ...\n", + module_name="constrained", + ) + complete_semantic_policies(module) + + with pytest.raises(ValueError, match="no runtime validators for semantic constraints"): + WrapperPlanner().build(module) diff --git a/tests/shared/types/test_mapping_report.py b/tests/fortran/infrastructure/types/test_mapping_report.py similarity index 98% rename from tests/shared/types/test_mapping_report.py rename to tests/fortran/infrastructure/types/test_mapping_report.py index 14d291a67..4b169c828 100644 --- a/tests/shared/types/test_mapping_report.py +++ b/tests/fortran/infrastructure/types/test_mapping_report.py @@ -110,7 +110,7 @@ def test_type_mapping_report_direct_script_runs_its_no_argument_example(): completed = subprocess.run( [sys.executable, "prik/probes/report.py"], - cwd=Path(__file__).resolve().parents[3], + cwd=Path(__file__).resolve().parents[4], capture_output=True, text=True, check=True, diff --git a/tests/shared/types/test_numpy.py b/tests/fortran/infrastructure/types/test_numpy.py similarity index 100% rename from tests/shared/types/test_numpy.py rename to tests/fortran/infrastructure/types/test_numpy.py diff --git a/tests/shared/architecture/test_visitor_protocol.py b/tests/fortran/infrastructure/utilities/test_class_visitor.py similarity index 100% rename from tests/shared/architecture/test_visitor_protocol.py rename to tests/fortran/infrastructure/utilities/test_class_visitor.py diff --git a/tests/shared/utilities/test_strings.py b/tests/fortran/infrastructure/utilities/test_strings.py similarity index 100% rename from tests/shared/utilities/test_strings.py rename to tests/fortran/infrastructure/utilities/test_strings.py diff --git a/tests/fortran/memory_management/runtime/test_handle_lifecycle.py b/tests/fortran/memory_management/runtime/test_handle_lifecycle.py index 0aaf27452..ddb4a1520 100644 --- a/tests/fortran/memory_management/runtime/test_handle_lifecycle.py +++ b/tests/fortran/memory_management/runtime/test_handle_lifecycle.py @@ -1,19 +1,21 @@ """Runtime ownership, factory, close, and finalizer behavior for native handles.""" -from tests.fortran._support.native_array_handles import ( +import ctypes +import gc +import numpy as np +import prik +import pytest +from prik.runtime.handles import ( AllocatableArray, NativeArrayHandleBase, PointerArray, - _ArrayState, - _common_ops, _native_array_descriptor_handoff_for_binding, _native_array_handle_from_generated_ops, +) +from tests.fortran._support.native_array_handles import ( + _ArrayState, + _common_ops, _required_handoff_ops, - ctypes, - gc, - np, - pytest, - prik, ) diff --git a/tests/fortran/memory_management/runtime/test_native_array_actual_handoff.py b/tests/fortran/memory_management/runtime/test_native_array_actual_handoff.py index de075ef83..8bf550ed7 100644 --- a/tests/fortran/memory_management/runtime/test_native_array_actual_handoff.py +++ b/tests/fortran/memory_management/runtime/test_native_array_actual_handoff.py @@ -1,15 +1,15 @@ """Runtime handoff validation for arrays and native storage handles.""" -from tests.fortran._support.native_array_handles import ( +import numpy as np +import pytest +from prik.runtime.handles import ( AllocatableArray, PointerArray, _NativeArrayHandoff, - _handoff, _native_array_actual_argument_for_binding_positional, _native_array_actual_for_binding, - np, - pytest, ) +from tests.fortran._support.native_array_handles import _handoff def test_array_actual_hook_rejects_generated_none_handoff(): diff --git a/tests/fortran/modules/policy/test_module_variable_policy.py b/tests/fortran/modules/policy/test_module_variable_policy.py index 1662e875e..3497dc6c0 100644 --- a/tests/fortran/modules/policy/test_module_variable_policy.py +++ b/tests/fortran/modules/policy/test_module_variable_policy.py @@ -1,10 +1,8 @@ """Completed policy for editable module-variable initialization.""" -from tests.fortran._support.ownership_policy import ( - SetterAction, - complete_semantic_policies, - parse_pyi_text, -) +from prik.semantics.ownership import SetterAction +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.ownership_policy import parse_pyi_text from prik.parsers.fortran.parser import parse_fortran_project from prik.pipeline.build import _apply_source_python_exports, _merge_wrapper_modules from prik.codegen.printers.pyi_printer import PyiPrinter diff --git a/tests/fortran/modules/semantics/test_module_contract_semantics.py b/tests/fortran/modules/semantics/test_module_contract_semantics.py index 198555eef..77e518211 100644 --- a/tests/fortran/modules/semantics/test_module_contract_semantics.py +++ b/tests/fortran/modules/semantics/test_module_contract_semantics.py @@ -1,9 +1,7 @@ """Semantic contracts for module state and common blocks.""" -from tests.fortran._support.semantic_conversion import ( - fortran_module_to_semantic_module, - parse_fortran_source, -) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik import parse_fortran_file as parse_fortran_source def test_module_common_block_storage_stays_internal(): diff --git a/tests/fortran/modules/semantics/test_modules_and_imports.py b/tests/fortran/modules/semantics/test_modules_and_imports.py index b8d2c2789..92f750c3b 100644 --- a/tests/fortran/modules/semantics/test_modules_and_imports.py +++ b/tests/fortran/modules/semantics/test_modules_and_imports.py @@ -1,21 +1,25 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( +from prik.parsers.fortran.models import ( FortranArgument, FortranModule, +) +from prik.semantics.fortran2ir import ( FortranToIRConverter, _requirement_unit_name, _resolve_compile_time_text, - array_contract, fortran_file_to_semantic_modules, fortran_module_to_semantic_module, +) +from tests.fortran._support.semantic_conversion import ( + array_contract, get_class, get_function, has_constraint, - parse_fortran_source, ) from prik import parse_fortran_project from prik.semantics.fortran2ir import fortran_project_to_semantic_modules +from prik import parse_fortran_file as parse_fortran_source def test_converter_normalizes_wrapped_types_and_resolves_wildcard_imports(): diff --git a/tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py b/tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py index 06f3f8945..436e1bc31 100644 --- a/tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py +++ b/tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py @@ -1,14 +1,14 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.metadata import SCALAR_STORAGE_CATEGORY +from prik.semantics.models import ProjectionMapping from tests.fortran._support.semantic_conversion import ( - ProjectionMapping, - SCALAR_STORAGE_CATEGORY, array_contract, - fortran_module_to_semantic_module, get_function, - parse_fortran_source, ) from prik.semantics.metadata import PROJECTED_OUTPUT_METADATA +from prik import parse_fortran_file as parse_fortran_source def test_optional_argument(): diff --git a/tests/fortran/optional_arguments/semantics/test_optional_pyi_semantics.py b/tests/fortran/optional_arguments/semantics/test_optional_pyi_semantics.py index 3fbee4dd0..16b5d2f99 100644 --- a/tests/fortran/optional_arguments/semantics/test_optional_pyi_semantics.py +++ b/tests/fortran/optional_arguments/semantics/test_optional_pyi_semantics.py @@ -1,11 +1,9 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - native_array_descriptor_kind, - parse_pyi_text, - pytest, - re, -) +import pytest +import re +from prik.semantics.native_array_handles import native_array_descriptor_kind +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_accepts_defaulted_scalar_descriptor_optional_dummies(): diff --git a/tests/fortran/pointers/policy/test_pointer_ownership_policy.py b/tests/fortran/pointers/policy/test_pointer_ownership_policy.py index 51a89833d..d4077805f 100644 --- a/tests/fortran/pointers/policy/test_pointer_ownership_policy.py +++ b/tests/fortran/pointers/policy/test_pointer_ownership_policy.py @@ -1,17 +1,28 @@ """Completed pointer ownership and native-array handle policy.""" -from tests.fortran._support.ownership_policy import ( - CodegenAction, - DestructionPolicy, +import pytest +from prik.codegen.printers import PyiPrinter +from prik.semantics.models import ( + RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, + RESOLVED_OWNERSHIP_POLICY_METADATA, +) +from prik.semantics.native_array_handles import ( NativeArrayBuildRequirement, NativeArrayHandlePolicyDispatcher, + native_array_handle_build_requirements, +) +from prik.semantics.ownership import ( + CodegenAction, + DestructionPolicy, NativeBarrierAction, OwnershipContext, OwnershipOwner, - PyiPrinter, - RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, - RESOLVED_OWNERSHIP_POLICY_METADATA, TransferMode, + default_ownership_policy, + set_ownership_metadata, +) +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.ownership_policy import ( _array_type, _derived_type, _hidden_output_context, @@ -20,12 +31,7 @@ _scalar_type, _string_type, _writable_argument_context, - complete_semantic_policies, - default_ownership_policy, - native_array_handle_build_requirements, parse_pyi_text, - pytest, - set_ownership_metadata, ) diff --git a/tests/fortran/pointers/runtime/test_pointer_array_actual_abi.py b/tests/fortran/pointers/runtime/test_pointer_array_actual_abi.py index 3f9687d00..fb9d8d78a 100644 --- a/tests/fortran/pointers/runtime/test_pointer_array_actual_abi.py +++ b/tests/fortran/pointers/runtime/test_pointer_array_actual_abi.py @@ -1,12 +1,14 @@ """Pointer array-actual handoff behavior.""" -from tests.fortran._support.native_array_handles import ( +import numpy as np +import pytest +from prik.runtime.handles import ( PointerArray, + _native_array_actual_argument_for_binding_positional, +) +from tests.fortran._support.native_array_handles import ( _ArrayState, _handoff, - _native_array_actual_argument_for_binding_positional, - np, - pytest, ) diff --git a/tests/fortran/pointers/runtime/test_pointer_descriptor_abi.py b/tests/fortran/pointers/runtime/test_pointer_descriptor_abi.py index 895e7bf02..182b34371 100644 --- a/tests/fortran/pointers/runtime/test_pointer_descriptor_abi.py +++ b/tests/fortran/pointers/runtime/test_pointer_descriptor_abi.py @@ -1,23 +1,25 @@ """Pointer descriptor handoff and strided-view ABI behavior.""" -from tests.fortran._support.native_array_handles import ( +import numpy as np +import pytest +from prik.runtime.handles import ( AllocatableArray, PointerArray, - _ArrayState, _NativeArrayDescriptorHandoff, - _common_ops, - _handoff, _native_array_descriptor_argument_for_binding, _native_array_descriptor_argument_for_binding_positional, _native_array_descriptor_for_binding, _native_array_descriptor_handoff_for_binding, _native_array_descriptor_handoff_for_binding_positional, _numpy_view_from_pointer_c_descriptor, +) +from tests.fortran._support.native_array_handles import ( + _ArrayState, + _common_ops, + _handoff, _pointer_descriptor_for_array, _pointer_descriptor_record_for_array, _required_handoff_ops, - np, - pytest, ) diff --git a/tests/fortran/pointers/runtime/test_pointer_handle_protocol.py b/tests/fortran/pointers/runtime/test_pointer_handle_protocol.py index c5b8d5857..fd2c2bacf 100644 --- a/tests/fortran/pointers/runtime/test_pointer_handle_protocol.py +++ b/tests/fortran/pointers/runtime/test_pointer_handle_protocol.py @@ -1,19 +1,21 @@ """Pointer handle state, association, extraction, and operation protocols.""" -from tests.fortran._support.native_array_handles import ( +import numpy as np +import pytest +from prik.runtime.handles import ( AllocatableArray, NativeArrayHandleBase, PointerArray, - _ArrayState, - _common_ops, - _handoff, _native_array_actual_for_binding, _native_array_descriptor_for_binding, _native_array_handle_from_generated_ops, +) +from tests.fortran._support.native_array_handles import ( + _ArrayState, + _common_ops, + _handoff, _pointer_descriptor_for_array, _required_handoff_ops, - np, - pytest, ) diff --git a/tests/fortran/pointers/semantics/test_pointer_semantics.py b/tests/fortran/pointers/semantics/test_pointer_semantics.py index ea0918c16..15c418f08 100644 --- a/tests/fortran/pointers/semantics/test_pointer_semantics.py +++ b/tests/fortran/pointers/semantics/test_pointer_semantics.py @@ -5,14 +5,14 @@ import pytest from tests.fortran._support.ownership_policy import parse_pyi_text +from prik.semantics.fortran2ir import fortran_module_to_semantic_module from tests.fortran._support.semantic_conversion import ( array_contract, - fortran_module_to_semantic_module, get_function, - parse_fortran_source, ) from prik.semantics.metadata import NATIVE_ARRAY_DESCRIPTOR_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA from prik.semantics.native_array_handles import native_array_descriptor_kind +from prik import parse_fortran_file as parse_fortran_source def test_fortran_pointer_arrays_and_scalars_preserve_descriptor_semantics(): diff --git a/tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py b/tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py index 9cc36ace2..0f8dcbb29 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py +++ b/tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py @@ -1,6 +1,6 @@ """Completed policy for editable exports and module initialization.""" -from tests.fortran._support.ownership_policy import ( +from prik.semantics.models import ( POLICY_COMPLETION_PREPARED_METADATA, PYTHON_EXPORTS_METADATA, PYTHON_EXPORTS_PREPARED_METADATA, @@ -10,10 +10,10 @@ SemanticModule, SemanticType, SemanticVariable, - SetterAction, - _scalar_type, - complete_semantic_policies, ) +from prik.semantics.ownership import SetterAction +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.ownership_policy import _scalar_type from prik.semantics.models import RESOLVED_MODULE_VARIABLE_POLICY_METADATA diff --git a/tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py b/tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py index 37929a640..bd0acd6fd 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py +++ b/tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py @@ -3,8 +3,7 @@ import ast import pytest - -from tests.fortran._support.semantic_conversion import parse_pyi_text +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text def test_mutable_module_literal_defaults_are_preserved(): diff --git a/tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py b/tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py index 6f33bd24f..493d2c671 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py +++ b/tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py @@ -1,15 +1,15 @@ """Semantic meaning of edited methods, overloads, and constructors.""" -from tests.fortran._support.pyi_conversion import ( +import pytest +import re +from dataclasses import asdict +from prik.codegen.printers import emit_module +from prik.semantics.metadata import ( BIND_TARGET_METADATA, SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA, USER_PRIVATE_METADATA, - asdict, - emit_module, - parse_pyi_text, - pytest, - re, ) +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_private_method_preserves_pass_projection_and_native_target(): diff --git a/tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py b/tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py index 5b3bed339..e938bf6a9 100644 --- a/tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py +++ b/tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py @@ -1,13 +1,13 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( +import pytest +from prik.codegen.printers import emit_module +from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, ADDRESS_ROLE_RAW, - complete_semantic_policies, - emit_module, - parse_pyi_text, - pytest, ) +from prik.semantics.policy_completion import complete_semantic_policies +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_public_raw_address_contract_round_trips(): diff --git a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py b/tests/fortran/semantic_ir/semantics/test_compile_time_values.py index a70f65c09..cdefeb3af 100644 --- a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py +++ b/tests/fortran/semantic_ir/semantics/test_compile_time_values.py @@ -4,7 +4,8 @@ import subprocess import sys -from tests.fortran._support.semantic_conversion import ( +import pytest +from prik.parsers.fortran.models import ( FortranArgument, FortranBlockData, FortranDerivedType, @@ -15,25 +16,28 @@ FortranProject, FortranSubmodule, FortranVariable, - ProjectionMapping, - SemanticArgument, - SemanticClass, - SemanticFunction, - SemanticMethod, - SemanticModule, - SemanticType, +) +from prik.semantics.fortran2ir import ( _compile_time_requirement_message, _iter_fortran_variable_contexts, collect_semantic_compile_time_requirements, fortran_file_to_semantic_modules, fortran_module_to_semantic_module, fortran_project_to_semantic_modules, - get_function, - parse_fortran_source, - pytest, resolve_semantic_compile_time_values, - semantic_models, ) +from prik.semantics.models import ( + ProjectionMapping, + SemanticArgument, + SemanticClass, + SemanticFunction, + SemanticMethod, + SemanticModule, + SemanticType, +) +from tests.fortran._support.semantic_conversion import get_function +from prik import parse_fortran_file as parse_fortran_source +from prik.semantics import models as semantic_models def test_fortran_to_ir_direct_script_runs_its_no_argument_example(): diff --git a/tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py b/tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py index c7f07508f..15309f474 100644 --- a/tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py +++ b/tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py @@ -1,13 +1,11 @@ """Tests split by stable ownership concept from `test_c_conversion_properties.py`.""" -from tests.fortran._support.semantic_properties import ( - asdict, - fortran_file_to_semantic_modules, - fortran_scalar_subroutines, - given, - parse_fortran_file, - pytest, -) +import pytest +from dataclasses import asdict +from hypothesis import given +from prik import parse_fortran_file +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from tests.fortran._support.semantic_properties import fortran_scalar_subroutines @pytest.mark.property diff --git a/tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py b/tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py index cc253e3dd..97c805ac8 100644 --- a/tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py +++ b/tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py @@ -3,7 +3,11 @@ import pytest -from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES, TESTS_DIR, parse_fixture +from tests.fortran._support.fixture_outputs import ( + PARSER_FIXTURE_ROOT as TESTS_DIR, + parse_fixture, +) +from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES from tests.fortran._support.fixture_outputs import ( SEMANTICS_FIXTURE_DIR, semantic_payload_for_fixture, diff --git a/tests/fortran/semantic_ir/semantics/test_semantic_specialization_properties.py b/tests/fortran/semantic_ir/semantics/test_semantic_specialization_properties.py index 60e4fd619..be53ed0b5 100644 --- a/tests/fortran/semantic_ir/semantics/test_semantic_specialization_properties.py +++ b/tests/fortran/semantic_ir/semantics/test_semantic_specialization_properties.py @@ -1,21 +1,25 @@ """Tests split by stable ownership concept from `test_c_conversion_properties.py`.""" -from tests.fortran._support.semantic_properties import ( +import pytest +from dataclasses import asdict +from hypothesis import ( + given, + strategies as st, +) +from prik import parse_fortran_file +from prik.semantics.fortran2ir import ( + fortran_file_to_semantic_modules, + resolve_semantic_compile_time_values, +) +from prik.semantics.models import ( SemanticArgument, SemanticArrayContract, SemanticConstraint, SemanticModule, SemanticStorageContract, SemanticType, - _FORTRAN_VALUE_TYPES, - asdict, - fortran_file_to_semantic_modules, - given, - parse_fortran_file, - pytest, - resolve_semantic_compile_time_values, - st, ) +from tests.fortran._support.semantic_properties import _FORTRAN_VALUE_TYPES @pytest.mark.property diff --git a/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py b/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py index 29a890726..406d4342b 100644 --- a/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py +++ b/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py @@ -4,22 +4,26 @@ import subprocess import sys -from tests.fortran._support.pyi_conversion import ( - CONTRACT_IMPORT, - CONTRACT_SYMBOLS, +import ast +import pytest +from prik.codegen.printers import emit_module +from prik.contracts import CONTRACT_SYMBOLS +from prik.semantics.models import ( ProjectionMapping, SemanticArgument, SemanticConstraint, SemanticModule, SemanticType, +) +from prik.semantics.pyi2ir import ( _PyiAstParser, - ast, convert_pyi_to_ir, - emit_module, - parse_pyi_ast_text, +) +from tests.fortran._support.pyi_conversion import ( + CONTRACT_IMPORT, parse_pyi_text, - pytest, ) +from prik.parsers.pyi import parse_pyi_text as parse_pyi_ast_text def test_pyi_parser_returns_python_ast_only(): diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py b/tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py index bbe1a9812..5ee1df7d6 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py @@ -1,8 +1,14 @@ """Tests split by stable ownership concept from `test_imports_and_packages.py`.""" -from tests.fortran._support.printer_models import ( - ProjectionMapping, +import pytest +from prik import parse_fortran_file as parse_fortran_source +from prik.codegen.printers import ( PyiPrinter, + emit_module, +) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.models import ( + ProjectionMapping, SemanticArgument, SemanticArrayContract, SemanticConstraint, @@ -11,14 +17,12 @@ SemanticStorageContract, SemanticType, SemanticVariable, - emit_module, - fortran_module_to_semantic_module, +) +from tests.fortran._support.printer_models import ( generate_pyi, generate_wrapper_artifacts, normalize, - parse_fortran_source, parse_pyi_text, - pytest, rendered_source, ) from prik.semantics.metadata import MAYBE_UNALLOCATED_METADATA diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py b/tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py index bea68b540..f574153fa 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py @@ -1,24 +1,28 @@ """Tests split by stable ownership concept from `test_imports_and_packages.py`.""" -from tests.fortran._support.printer_models import ( - OPERATOR_F90_SOURCE, - ProjectionMapping, +import pytest +from prik import parse_fortran_file as parse_fortran_source +from prik.codegen.printers import ( PyiPrinter, + emit_module, + emit_module_stubs, +) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.models import ( + ProjectionMapping, SemanticArgument, SemanticClass, SemanticFunction, SemanticMethod, SemanticModule, SemanticType, - emit_module, - emit_module_stubs, - fortran_module_to_semantic_module, +) +from tests.fortran._support.printer_models import ( + OPERATOR_F90_SOURCE, generate_pyi, generate_wrapper_artifacts, normalize, - parse_fortran_source, parse_pyi_text, - pytest, rendered_source, ) diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py index 3c1ed2b13..257d7c91c 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py @@ -5,7 +5,11 @@ from prik.semantics.fortran2ir import fortran_module_to_semantic_module from prik.codegen.printers import emit_module -from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES, TESTS_DIR, parse_fixture +from tests.fortran._support.fixture_outputs import ( + PARSER_FIXTURE_ROOT as TESTS_DIR, + parse_fixture, +) +from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES def test_pyi_printer_fixture_suite_has_fixtures(): diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py index f9fa026a5..5229534d8 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py @@ -1,7 +1,17 @@ """Tests split by stable ownership concept from `test_imports_and_packages.py`.""" -from tests.fortran._support.printer_models import ( +import prik +import pytest +from prik import parse_fortran_file as parse_fortran_source +from prik.codegen.printers import ( PyiPrinter, + emit_module, + emit_module_stubs, + opaque_dependency_modules, +) +from prik.pipeline.pyi import pyi_text_to_semantic_module as _parse_pyi_text +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.models import ( SemanticArgument, SemanticArrayContract, SemanticClass, @@ -14,16 +24,8 @@ SemanticStorageContract, SemanticType, SemanticVariable, - _parse_pyi_text, - emit_module, - emit_module_stubs, - fortran_module_to_semantic_module, - generate_pyi, - opaque_dependency_modules, - parse_fortran_source, - pytest, - prik, ) +from tests.fortran._support.printer_models import generate_pyi def test_prik_public_api_exports_module_stub_emitter(): diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_types_and_declarations.py b/tests/fortran/semantic_pyi_format/pipeline/test_types_and_declarations.py index d9b624efb..f712d090b 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_types_and_declarations.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_types_and_declarations.py @@ -1,8 +1,14 @@ """Tests split by stable ownership concept from `test_imports_and_packages.py`.""" -from tests.fortran._support.printer_models import ( - ProjectionMapping, +import pytest +from prik import parse_fortran_file as parse_fortran_source +from prik.codegen.printers import ( PyiPrinter, + emit_module, +) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.models import ( + ProjectionMapping, SemanticArgument, SemanticArrayContract, SemanticClass, @@ -13,12 +19,10 @@ SemanticOrigin, SemanticStorageContract, SemanticType, - emit_module, - fortran_module_to_semantic_module, +) +from tests.fortran._support.printer_models import ( generate_pyi, - parse_fortran_source, parse_pyi_text, - pytest, ) diff --git a/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py b/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py index 9cb7e82ac..120dd5102 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py @@ -1,26 +1,28 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( +import ast +import pytest +from dataclasses import asdict +from prik import parse_fortran_file +from prik.codegen.printers import emit_module +from prik.contracts import CONTRACT_SYMBOLS +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, ADDRESS_ROLE_PROJECTION, - CONTRACT_SYMBOLS, PROJECTED_OUTPUT_METADATA, +) +from prik.semantics.models import ( ProjectionMapping, SemanticArgument, SemanticConstraint, SemanticFunction, SemanticModule, SemanticType, - _PyiAstParser, - asdict, - ast, - complete_semantic_policies, - emit_module, - fortran_file_to_semantic_modules, - parse_fortran_file, - parse_pyi_text, - pytest, ) +from prik.semantics.policy_completion import complete_semantic_policies +from prik.semantics.pyi2ir import _PyiAstParser +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_accepts_ast_only_projection_value_refs(): diff --git a/tests/fortran/semantic_pyi_format/semantics/test_classes_and_overloads.py b/tests/fortran/semantic_pyi_format/semantics/test_classes_and_overloads.py index 06900dab4..5f5e30136 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_classes_and_overloads.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_classes_and_overloads.py @@ -1,9 +1,7 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - emit_module, - parse_pyi_text, -) +from prik.codegen.printers import emit_module +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_applies_decorators_after_native_call(): diff --git a/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py b/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py index 7fc54b5be..6c2727910 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py @@ -1,21 +1,25 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - CONTRACT_IMPORT, - Path, - SemanticImport, - SemanticImportItem, - emit_module, - fortran_file_to_semantic_modules, - native_contract_issues, - parse_fortran_file, - parse_pyi_text, +import pytest +from pathlib import Path +from prik import parse_fortran_file +from prik.codegen.printers import emit_module +from prik.pipeline.pyi import ( pyi_file_to_semantic_module, pyi_paths_to_semantic_modules, - pyi_pipeline, pyi_text_to_semantic_module, - pytest, ) +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from prik.semantics.models import ( + SemanticImport, + SemanticImportItem, +) +from prik.semantics.native_contract import native_contract_issues +from tests.fortran._support.pyi_conversion import ( + CONTRACT_IMPORT, + parse_pyi_text, +) +import prik.pipeline.pyi as pyi_pipeline def test_convert_pyi_to_ir_requires_imported_contract_types(): diff --git a/tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py b/tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py index b309db493..19f28f216 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py @@ -1,20 +1,24 @@ """Tests split by stable ownership concept from `test_c_conversion_properties.py`.""" -from tests.fortran._support.semantic_properties import ( +import pytest +from hypothesis import ( + given, + strategies as st, +) +from prik.codegen.printers import emit_module +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text +from prik.semantics.models import ( EXTERNAL_TYPE_REF_METADATA, ProjectionMapping, SemanticArgument, SemanticFunction, SemanticModule, SemanticType, +) +from tests.fortran._support.semantic_properties import ( _NATIVE_NAMES, _PYI_IDENTIFIER_STEMS, canonical_semantic_types, - emit_module, - given, - parse_pyi_text, - pytest, - st, ) diff --git a/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py b/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py index 128a6cbc8..741237cc3 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py @@ -1,30 +1,34 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( +import ast +import pytest +from prik import parse_fortran_file +from prik.codegen.printers import emit_module +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, ADDRESS_ROLE_RAW, NATIVE_ARRAY_DESCRIPTOR_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, + USER_PRIVATE_METADATA, +) +from prik.semantics.models import ( PYTHON_VALUE_IMMUTABLE, PYTHON_VALUE_MUTABILITY_METADATA, SemanticConstraint, SemanticField, SemanticVariable, - USER_PRIVATE_METADATA, - _node_text, - ast, - emit_module, - fortran_file_to_semantic_modules, +) +from prik.semantics.native_array_handles import ( is_native_array_handle, native_array_data_type, native_array_descriptor_kind, native_array_handle_facts, - native_contract_issues, - parse_fortran_file, - parse_pyi_text, - pyi_text_to_semantic_module, - pytest, ) +from prik.semantics.native_contract import native_contract_issues +from prik.semantics.pyi2ir import _node_text +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_convert_pyi_to_ir_dispatches_nested_and_qualified_semantic_types(): diff --git a/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py b/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py index 630786a00..9cba6910b 100644 --- a/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py +++ b/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py @@ -1,16 +1,18 @@ """Tests split by stable ownership concept from `test_source_form_and_diagnostics_regressions.py`.""" -from tests.fortran._support.parser_regressions import ( +import pytest +from pathlib import Path +from prik import FortranParseError +from prik.parsers.fortran.models import ( FortranArgument, FortranDerivedType, FortranModule, - FortranParseError, - FortranParser, FortranProcedureSignature, - Path, +) +from prik.parsers.fortran.parser import ( + FortranParser, _ParserScope, parse_fortran_project, - pytest, ) diff --git a/tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py b/tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py index 4e0d0659f..a13cc7a15 100644 --- a/tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py +++ b/tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py @@ -1,15 +1,17 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( +import pytest +from prik import ( FortranParseError, - parse_fortran_block_data_unit, parse_fortran_file, + parse_fortran_project, +) +from tests.fortran._support.parser_procedures import ( + parse_fortran_block_data_unit, parse_fortran_module, parse_fortran_modules, parse_fortran_program, - parse_fortran_project, parse_fortran_submodule, - pytest, ) diff --git a/tests/fortran/source_parsing/parsing/test_fortran_parser_procedures_and_interfaces.py b/tests/fortran/source_parsing/parsing/test_fortran_parser_procedures_and_interfaces.py index 68c000d1f..c36954bbe 100644 --- a/tests/fortran/source_parsing/parsing/test_fortran_parser_procedures_and_interfaces.py +++ b/tests/fortran/source_parsing/parsing/test_fortran_parser_procedures_and_interfaces.py @@ -1,17 +1,21 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( +import pytest +from prik import ( + parse_fortran_file, + parse_fortran_project, +) +from prik.parsers.fortran.models import ( FortranFunctionCall, FortranSlice, FortranVariable, +) +from tests.fortran._support.parser_procedures import ( collect_project_procedure_signatures, parse_fortran_block_data, - parse_fortran_file, parse_fortran_modules, parse_fortran_programs, - parse_fortran_project, parse_fortran_submodules, - pytest, ) diff --git a/tests/fortran/source_parsing/parsing/test_fortran_parser_properties.py b/tests/fortran/source_parsing/parsing/test_fortran_parser_properties.py index 314bede65..78768bbba 100644 --- a/tests/fortran/source_parsing/parsing/test_fortran_parser_properties.py +++ b/tests/fortran/source_parsing/parsing/test_fortran_parser_properties.py @@ -1,18 +1,20 @@ """Tests split by stable ownership concept from `test_properties.py`.""" -from tests.fortran._support.parser_properties import ( +import pytest +from contextlib import suppress +from hypothesis import given +from prik import ( FortranParseError, + parse_fortran_file, +) +from prik.codegen.printers import emit_module_stubs +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from tests.fortran._support.parser_properties import ( _FORTRAN_IDENTIFIER_STEMS, _FORTRAN_SCALAR_TYPES, _FUZZ_TEXT, - emit_module_stubs, - fortran_file_to_semantic_modules, fortran_subroutines, - given, - parse_fortran_file, - parse_pyi_text, - pytest, - suppress, ) diff --git a/tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py b/tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py index 518b61dec..14622b2cd 100644 --- a/tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py +++ b/tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py @@ -1,20 +1,26 @@ """Tests split by stable ownership concept from `test_source_form_and_diagnostics_regressions.py`.""" -from tests.fortran._support.parser_regressions import ( +import pytest +from pathlib import Path +from prik import ( + FortranParseError, + parse_fortran_file, +) +from prik.parsers.fortran.models import ( FortranArgument, FortranDerivedType, FortranModule, - FortranParseError, - FortranParser, FortranProcedureSignature, - Path, +) +from prik.parsers.fortran.parser import ( + FortranParser, _ParserScope, _UnitParts, +) +from tests.fortran._support.parser_regressions import ( _empty_unit, _lines, _unit, - parse_fortran_file, - pytest, ) diff --git a/tests/fortran/source_preprocessing/preprocessing/_support.py b/tests/fortran/source_preprocessing/preprocessing/_support.py index b5bdd7ced..60fb02dbb 100644 --- a/tests/fortran/source_preprocessing/preprocessing/_support.py +++ b/tests/fortran/source_preprocessing/preprocessing/_support.py @@ -1,31 +1,12 @@ -"""Shared preprocessing CLI and compiler-command contract tests.""" - -import json +"""Reusable fake-compiler and error assertions for preprocessing tests.""" import os - -import subprocess - import sys - from pathlib import Path import pytest -import prik.pipeline.preprocessing as preprocessing - -from prik.pipeline.preprocessing import ( - PreprocessingError, - PreprocessingConfig, - build_compile_commands_invocation, - build_direct_preprocess_invocation, - build_preprocess_invocation, - build_template_preprocess_invocation, - expand_native_fortran_includes, - run_compiler_preprocessor, - run_compiler_preprocessor_with_recipe, - validate_macro_name, -) +from prik.pipeline.preprocessing import PreprocessingError def _fake_compiler(tmp_path: Path, output: str) -> tuple[Path, Path, dict[str, str]]: @@ -75,26 +56,3 @@ def _assert_preprocessing_error( assert str(exc_info.value) == message assert exc_info.value.category == category assert exc_info.value.diagnostics == [] - - -__all__ = ( - "Path", - "PreprocessingConfig", - "PreprocessingError", - "_assert_preprocessing_error", - "_failing_compiler", - "_fake_compiler", - "build_compile_commands_invocation", - "build_direct_preprocess_invocation", - "build_preprocess_invocation", - "build_template_preprocess_invocation", - "expand_native_fortran_includes", - "json", - "preprocessing", - "pytest", - "run_compiler_preprocessor", - "run_compiler_preprocessor_with_recipe", - "subprocess", - "sys", - "validate_macro_name", -) diff --git a/tests/fortran/source_preprocessing/preprocessing/test_cli.py b/tests/fortran/source_preprocessing/preprocessing/test_cli.py index 0d0abeb7b..2ce3ce4cf 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_cli.py +++ b/tests/fortran/source_preprocessing/preprocessing/test_cli.py @@ -1,12 +1,11 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.fortran.source_preprocessing.preprocessing._support import ( - Path, - _fake_compiler, - json, - subprocess, - sys, -) +import json +from pathlib import Path +import subprocess +import sys + +from tests.fortran.source_preprocessing.preprocessing._support import _fake_compiler def test_cli_help_documents_exact_compiler_and_preprocessing_examples(): diff --git a/tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py b/tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py index 2960ea043..5d4fc2b01 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py +++ b/tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py @@ -1,19 +1,21 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.fortran.source_preprocessing.preprocessing._support import ( - Path, +import json +from pathlib import Path + +import pytest + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingError, - _assert_preprocessing_error, build_direct_preprocess_invocation, build_preprocess_invocation, expand_native_fortran_includes, - json, - preprocessing, - pytest, run_compiler_preprocessor_with_recipe, validate_macro_name, ) +from tests.fortran.source_preprocessing.preprocessing._support import _assert_preprocessing_error def test_direct_fortran_preprocess_invocation_uses_exact_compiler_and_cpp(tmp_path: Path): diff --git a/tests/fortran/source_preprocessing/preprocessing/test_dependencies_and_includes.py b/tests/fortran/source_preprocessing/preprocessing/test_dependencies_and_includes.py index cbd35ec52..f83eaa34f 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_dependencies_and_includes.py +++ b/tests/fortran/source_preprocessing/preprocessing/test_dependencies_and_includes.py @@ -1,11 +1,9 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.fortran.source_preprocessing.preprocessing._support import ( - Path, - PreprocessingConfig, - expand_native_fortran_includes, - preprocessing, -) +from pathlib import Path + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import PreprocessingConfig, expand_native_fortran_includes def test_linemarker_included_files_fortran_metadata_and_duplicate_entries(tmp_path: Path): diff --git a/tests/fortran/source_preprocessing/preprocessing/test_execution.py b/tests/fortran/source_preprocessing/preprocessing/test_execution.py index eb1325109..68b870e2c 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_execution.py +++ b/tests/fortran/source_preprocessing/preprocessing/test_execution.py @@ -1,11 +1,13 @@ """Tests split by stable ownership concept from `test_cli.py`.""" -from tests.fortran.source_preprocessing.preprocessing._support import ( - Path, +from pathlib import Path + +import pytest + +import prik.pipeline.preprocessing as preprocessing +from prik.pipeline.preprocessing import ( PreprocessingConfig, PreprocessingError, - preprocessing, - pytest, ) diff --git a/tests/fortran/source_preprocessing/preprocessing/test_preprocessing_properties.py b/tests/fortran/source_preprocessing/preprocessing/test_preprocessing_properties.py index b0ff11ab3..befbca04c 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_preprocessing_properties.py +++ b/tests/fortran/source_preprocessing/preprocessing/test_preprocessing_properties.py @@ -1,20 +1,24 @@ """Tests split by stable ownership concept from `test_properties.py`.""" -from tests.fortran._support.parser_properties import ( - FortranParseError, - Path, - PreprocessingConfig, - TemporaryDirectory, - _FORTRAN_IDENTIFIER_STEMS, +import prik.pipeline.preprocessing as preprocessing +import pytest +import sys +from hypothesis import ( given, + strategies as st, +) +from pathlib import Path +from prik import ( + FortranParseError, parse_fortran_file, - patch, +) +from prik.pipeline.preprocessing import ( + PreprocessingConfig, preprocess_source, - preprocessing, - pytest, - st, - sys, ) +from tempfile import TemporaryDirectory +from unittest.mock import patch +from tests.fortran._support.parser_properties import _FORTRAN_IDENTIFIER_STEMS @pytest.mark.property diff --git a/tests/fortran/strings/parsing/test_character_declarations.py b/tests/fortran/strings/parsing/test_character_declarations.py index b4e1b08e0..51073bcf5 100644 --- a/tests/fortran/strings/parsing/test_character_declarations.py +++ b/tests/fortran/strings/parsing/test_character_declarations.py @@ -1,8 +1,6 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( - parse_fortran_file, -) +from prik import parse_fortran_file def test_legacy_character_and_star_kind_declarations_from_inline_fortran(): diff --git a/tests/fortran/strings/parsing/test_fixed_form_character_parsing.py b/tests/fortran/strings/parsing/test_fixed_form_character_parsing.py index 39804c971..34bbe7905 100644 --- a/tests/fortran/strings/parsing/test_fixed_form_character_parsing.py +++ b/tests/fortran/strings/parsing/test_fixed_form_character_parsing.py @@ -1,8 +1,6 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" -from tests.fortran._support.parser_procedures import ( - parse_fortran_file, -) +from prik import parse_fortran_file def test_fixed_form_character_star_length_is_parsed(): diff --git a/tests/fortran/strings/semantics/test_fortran_string_semantics.py b/tests/fortran/strings/semantics/test_fortran_string_semantics.py index 173fde632..b31bf0913 100644 --- a/tests/fortran/strings/semantics/test_fortran_string_semantics.py +++ b/tests/fortran/strings/semantics/test_fortran_string_semantics.py @@ -1,10 +1,8 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - fortran_module_to_semantic_module, - get_function, - parse_fortran_source, -) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from tests.fortran._support.semantic_conversion import get_function +from prik import parse_fortran_file as parse_fortran_source def test_scalar_character_inout_is_projected_as_replacement_return(): diff --git a/tests/fortran/strings/semantics/test_string_pyi_semantics.py b/tests/fortran/strings/semantics/test_string_pyi_semantics.py index 89b7bafe6..776cb0b84 100644 --- a/tests/fortran/strings/semantics/test_string_pyi_semantics.py +++ b/tests/fortran/strings/semantics/test_string_pyi_semantics.py @@ -1,10 +1,8 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" -from tests.fortran._support.pyi_conversion import ( - emit_module, - parse_pyi_text, - pytest, -) +import pytest +from prik.codegen.printers import emit_module +from tests.fortran._support.pyi_conversion import parse_pyi_text def test_rank_zero_string_storage_round_trips_as_empty_tuple_array(): diff --git a/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py b/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py index 0f233d262..482622bcd 100644 --- a/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py +++ b/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py @@ -1,12 +1,10 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from tests.fortran._support.semantic_conversion import ( - ProjectionMapping, - fortran_module_to_semantic_module, - get_function, - parse_fortran_source, -) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.models import ProjectionMapping +from tests.fortran._support.semantic_conversion import get_function from prik.semantics.metadata import PROJECTED_OUTPUT_METADATA +from prik import parse_fortran_file as parse_fortran_source def test_primitive_scalar_inout_stays_visible_and_projects_replacement_return(): diff --git a/tests/shared/README.md b/tests/shared/README.md deleted file mode 100644 index 146059782..000000000 --- a/tests/shared/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Language-Neutral Tests - -`tests/shared/` owns only behavior that does not import, select, parameterize, -or fall back between C and Fortran. A test is not shared merely because both -languages currently use the same Python helper. - -Final shared owners include architecture, generic CLI parsing/output, -language-neutral semantic-contract syntax, documentation, naming, repository -tools, type utilities, and other genuinely language-independent mechanisms. -Language-specific behavior stays under `tests/fortran/` or `tests/c/`. - -The maintained shared owner directories are `architecture/`, `docs/`, -`naming/`, `tools/`, `types/`, and `utilities/`. Do not declare an empty -owner directory: benchmark cases stay with the feature or tool they measure -and use the `benchmark` marker when cross-suite selection is needed. - -Run the final shared owner independently with: - -```bash -python3 -m pytest -q tests/shared -``` - -Do not add cross-language forwarding imports or compatibility paths. diff --git a/tests/shared/architecture/test_dependency_boundaries.py b/tests/shared/architecture/test_dependency_boundaries.py deleted file mode 100644 index 57e0924e9..000000000 --- a/tests/shared/architecture/test_dependency_boundaries.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Structural contracts for navigable wrapper-generation boundaries.""" - -from __future__ import annotations - -import ast - -from tests.fortran._support.wrapper_build import REPO_ROOT - -CODEGEN_ROOT = REPO_ROOT / "prik" / "codegen" -BOUNDARY_MODULES = ( - ("c", CODEGEN_ROOT / "c" / "binding.py"), - ("fortran", CODEGEN_ROOT / "fortran" / "bridge.py"), - ("printers", CODEGEN_ROOT / "printers" / "pyi_printer.py"), - ("printers", CODEGEN_ROOT / "printers" / "source_printers.py"), -) -PUBLIC_MODULE_FUNCTIONS = { - ("printers", "pyi_printer.py", "emit_module"), - ("printers", "pyi_printer.py", "emit_module_stubs"), - ("printers", "pyi_printer.py", "opaque_dependency_modules"), -} - - -def test_codegen_boundary_entrypoints_and_visitors_are_documented(): - """Require public entrypoints and dispatched model visitors to state their contract.""" - missing = [] - for _, path in BOUNDARY_MODULES: - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - for node in tree.body: - if isinstance(node, ast.FunctionDef) and ast.get_docstring(node) is None: - missing.append(f"{path.name}:{node.lineno}:{node.name}") - if isinstance(node, ast.ClassDef): - missing.extend( - f"{path.name}:{method.lineno}:{node.name}.{method.name}" - for method in node.body - if isinstance(method, ast.FunctionDef) - and (not method.name.startswith("_") or method.name.startswith("_visit_")) - and ast.get_docstring(method) is None - ) - assert not missing, "Undocumented wrapper-codegen callables:\n" + "\n".join(missing) - - -def test_codegen_uses_one_model_visitor_protocol(): - """Prevent alternate printer and extractor dispatch protocols from appearing.""" - invalid = [] - lowercase_model_names = {"int", "str", "tuple"} - for _, path in BOUNDARY_MODULES: - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef) and node.name.startswith(("_print_", "_extract_")): - invalid.append(f"{path.name}:{node.lineno}:{node.name}") - if isinstance(node, ast.FunctionDef) and node.name.startswith("_visit_"): - model_name = node.name.removeprefix("_visit_") - if model_name[:1].islower() and model_name not in lowercase_model_names | {"not_supported"}: - invalid.append(f"{path.name}:{node.lineno}:{node.name}") - assert not invalid, "Use _visit_* handlers or named helpers:\n" + "\n".join(invalid) - - -def test_module_functions_are_deliberate_boundary_apis(): - """Keep stateful generation logic on its owning class.""" - unexpected = [] - for area, path in BOUNDARY_MODULES: - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - for node in (node for node in tree.body if isinstance(node, ast.FunctionDef)): - key = (area, path.name, node.name) - if key not in PUBLIC_MODULE_FUNCTIONS: - unexpected.append(f"{path.name}:{node.lineno}:{node.name}") - assert not unexpected, "Unexpected module-level wrapper-codegen functions:\n" + "\n".join(unexpected) diff --git a/tests/shared/architecture/test_package_structure.py b/tests/shared/architecture/test_package_structure.py deleted file mode 100644 index 4dbbe1ae5..000000000 --- a/tests/shared/architecture/test_package_structure.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Mechanical checks for the top-level prik package architecture.""" - -from pathlib import Path - - -PACKAGE_ROOT = Path(__file__).parents[3] / "prik" -ROOT_PYTHON_MODULES = {"__init__.py", "__main__.py", "cli.py", "stage_values.py"} - - -def test_prik_root_contains_only_public_entrypoint_modules(): - assert {path.name for path in PACKAGE_ROOT.glob("*.py")} == ROOT_PYTHON_MODULES diff --git a/tests/shared/architecture/test_test_suite_layout.py b/tests/shared/architecture/test_test_suite_layout.py deleted file mode 100644 index 7c2840400..000000000 --- a/tests/shared/architecture/test_test_suite_layout.py +++ /dev/null @@ -1,616 +0,0 @@ -"""Positive ownership and navigation contracts for the pytest tree.""" - -from __future__ import annotations - -import ast -import re -from pathlib import Path - - -REPO_ROOT = Path(__file__).parents[3] -TEST_ROOT = REPO_ROOT / "tests" -EXAMPLES_ROOT = REPO_ROOT / "examples" -TEST_INDEX = TEST_ROOT / "README.md" -WORKFLOW_ROOT = REPO_ROOT / ".github/workflows" -REAL_LIBRARIES_WORKFLOW = REPO_ROOT / ".github/workflows/real-libraries.yml" -CLAUDE_WORKFLOW = REPO_ROOT / ".github/workflows/claude.yml" -COVERAGE_WORKFLOW = REPO_ROOT / ".github/workflows/coverage.yml" -CODECOV_CONFIG = REPO_ROOT / "codecov.yml" -DOCS_WORKFLOW = REPO_ROOT / ".github/workflows/docs.yml" -FORTRAN_SMOKE_WORKFLOW = REPO_ROOT / ".github/workflows/fortran-toolchain-smoke.yml" -MERGE_VALIDATION_WORKFLOW = REPO_ROOT / ".github/workflows/merge-validation.yml" -PUBLISH_WORKFLOW = REPO_ROOT / ".github/workflows/publish-to-pypi.yml" -STATIC_ANALYSIS_WORKFLOW = REPO_ROOT / ".github/workflows/static-analysis.yml" -TESTS_WORKFLOW = REPO_ROOT / ".github/workflows/tests.yml" -QUALITY_ASSURANCE_DOC = REPO_ROOT / "docs/developer/quality-assurance.md" -PYPROJECT = REPO_ROOT / "pyproject.toml" -CHANGELOG = REPO_ROOT / "CHANGELOG.md" -MANIFEST = REPO_ROOT / "MANIFEST.in" -BLAS_CI_FULL_SURFACE = "examples/blas/ci/full_surface.py" -LAPACK_CI_FULL_SURFACE = "examples/lapack/ci/full_surface.py" - -LEGACY_TEST_ROOTS = { - "benchmarks", - "cli", - "data", - "docs", - "naming", - "parser", - "parsing", - "pipeline", - "probes", - "pyi", - "runtime", - "semantics", - "tools", - "types", - "utilities", - "wrapper", - "codegen", -} -LANGUAGE_DIRECTORIES = {"c", "fortran", "shared"} -PRIMARY_OWNER_DIRECTORIES = {"architecture", *LANGUAGE_DIRECTORIES} -ARCHITECTURE_OWNER_DIRECTORIES = {"c", "fortran"} -FORTRAN_OWNER_DIRECTORIES = { - "allocatables", - "arrays", - "building_shared_library", - "callbacks", - "command_line_interface", - "data_types", - "derived_types", - "enumerations", - "error_handling", - "functions", - "generic_interfaces", - "infrastructure", - "memory_management", - "modules", - "optional_arguments", - "pointers", - "pyi_contracts", - "raw_addresses", - "semantic_pyi_format", - "semantic_ir", - "source_parsing", - "source_preprocessing", - "strings", - "subroutines", -} -SHARED_OWNER_DIRECTORIES = {"architecture", "docs", "naming", "tools", "types", "utilities"} -TOOLS_TEST_MODULES = { - "test_benchmark_host.py", - "test_build_time_benchmark.py", - "test_check_benchmark_regression.py", - "test_check_radon_policy.py", - "test_check_static_analysis_versions.py", - "test_generate_performance_docs.py", - "test_print_pytest_failures.py", - "test_runtime_benchmark.py", - "test_run_fortran_toolchain_lane.py", - "test_warm_real_library_native_cache.py", -} -DOCS_TEST_MODULES = {"test_examples.py", "test_publication.py", "test_structure.py"} -ARCHITECTURE_TEST_MODULES = { - "test_dependency_boundaries.py", - "test_package_structure.py", - "test_test_suite_layout.py", - "test_visitor_protocol.py", -} -STALE_PYTEST_PATH_PATTERNS = ( - *(re.compile(rf"\btests/{re.escape(root)}(?=/|\b)") for root in sorted(LEGACY_TEST_ROOTS)), - re.compile(r"\btests/_shared(?=/|\b)"), -) - - -def _pytest_modules() -> list[Path]: - return sorted(TEST_ROOT.rglob("test_*.py")) - - -def _maintained_path_reference_files() -> list[Path]: - candidates = [REPO_ROOT / "README.md", *REPO_ROOT.glob(".github/workflows/*"), *REPO_ROOT.rglob("*.md")] - return sorted( - path - for path in candidates - if path.is_file() - and path.name != "AGENTS.md" - and "old_docs" not in path.parts - and "_migration" not in path.parts - and "docs/maintainer/roadmap" not in path.as_posix() - and ".git" not in path.parts - ) - - -def _github_action_job_display_names(workflow: Path) -> dict[str, str]: - names: dict[str, str] = {} - current_job: str | None = None - in_jobs = False - for line in workflow.read_text(encoding="utf-8").splitlines(): - if line == "jobs:": - in_jobs = True - continue - if not in_jobs: - continue - job_match = re.fullmatch(r" ([a-z0-9-]+):", line) - if job_match: - current_job = job_match.group(1) - continue - if current_job is not None and line.startswith(" name: "): - names[current_job] = line.removeprefix(" name: ") - return names - - -def _github_action_workflow_name(workflow: Path) -> str: - first_line = workflow.read_text(encoding="utf-8").splitlines()[0] - assert first_line.startswith("name: ") - return first_line.removeprefix("name: ") - - -def _github_matrix_display_names(workflow: Path) -> tuple[str, ...]: - prefix = "display_name: " - return tuple( - line.strip().removeprefix(prefix) - for line in workflow.read_text(encoding="utf-8").splitlines() - if line.strip().startswith(prefix) - ) - - -def _github_action_job_block(workflow: Path, job_id: str) -> str: - remainder = workflow.read_text(encoding="utf-8").split(f" {job_id}:\n", maxsplit=1)[1] - next_job = re.search(r"\n [a-z0-9-]+:\n", remainder) - return remainder if next_job is None else remainder[: next_job.start()] - - -def _github_action_job_steps(workflow: Path, job_id: str) -> tuple[str, ...]: - block = _github_action_job_block(workflow, job_id) - steps = block.split(" steps:\n", maxsplit=1)[1] - return tuple(line for line in steps.splitlines() if line.strip()) - - -def test_pytest_modules_have_one_allowed_primary_owner() -> None: - root_modules = sorted(path.name for path in TEST_ROOT.glob("test_*.py")) - assert root_modules == [] - - assert { - path.name for path in TEST_ROOT.iterdir() if path.is_dir() and path.name != "__pycache__" - } == PRIMARY_OWNER_DIRECTORIES - assert all(path.relative_to(TEST_ROOT).parts[0] in PRIMARY_OWNER_DIRECTORIES for path in _pytest_modules()) - - -def test_primary_roots_use_only_documented_owner_directories() -> None: - for root, expected in ( - (TEST_ROOT / "architecture", ARCHITECTURE_OWNER_DIRECTORIES), - (TEST_ROOT / "fortran", FORTRAN_OWNER_DIRECTORIES), - (TEST_ROOT / "shared", SHARED_OWNER_DIRECTORIES), - ): - actual = { - path.name for path in root.iterdir() if path.is_dir() and path.name not in {"__pycache__", "_support"} - } - assert actual == expected - - -def test_stage_modules_have_unique_basenames_for_default_pytest_import_mode() -> None: - by_name: dict[str, list[str]] = {} - for path in _pytest_modules(): - by_name.setdefault(path.name, []).append(path.relative_to(TEST_ROOT).as_posix()) - duplicates = {name: paths for name, paths in by_name.items() if len(paths) > 1} - assert duplicates == {} - - -def test_language_first_roots_have_documented_existing_owners() -> None: - text = TEST_INDEX.read_text(encoding="utf-8") - for language in LANGUAGE_DIRECTORIES: - test_directory = f"tests/{language}/" - assert test_directory in text - assert (REPO_ROOT / test_directory).is_dir() - - -def test_specialized_test_lanes_contain_only_owned_modules() -> None: - assert {path.name for path in (TEST_ROOT / "shared" / "tools").glob("test_*.py")} == TOOLS_TEST_MODULES - assert {path.name for path in (TEST_ROOT / "shared" / "docs").glob("test_*.py")} == DOCS_TEST_MODULES - assert { - path.name for path in (TEST_ROOT / "shared" / "architecture").glob("test_*.py") - } == ARCHITECTURE_TEST_MODULES - - -def test_fortran_support_directory_contains_no_pytest_modules() -> None: - assert sorted((TEST_ROOT / "fortran" / "_support").rglob("test_*.py")) == [] - - -def test_native_library_examples_are_copyable_without_the_repository_test_package() -> None: - assert (EXAMPLES_ROOT / "__init__.py").is_file() - assert (EXAMPLES_ROOT / "conftest.py").is_file() - assert (EXAMPLES_ROOT / "blas" / "__init__.py").is_file() - assert (EXAMPLES_ROOT / "lapack" / "__init__.py").is_file() - - repository_test_imports = [] - for path in sorted(EXAMPLES_ROOT.rglob("*.py")): - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - for node in ast.walk(tree): - if isinstance(node, ast.ImportFrom) and node.module is not None and node.module.startswith("tests"): - repository_test_imports.append(f"{path.relative_to(REPO_ROOT)}: {node.module}") - if isinstance(node, ast.Import): - repository_test_imports.extend( - f"{path.relative_to(REPO_ROOT)}: {alias.name}" - for alias in node.names - if alias.name.startswith("tests") - ) - assert repository_test_imports == [] - - -def test_native_library_examples_separate_user_tests_from_ci_audits() -> None: - fixture_names = { - "blas": {"prik_blas", "f2py_blas"}, - "lapack": {"prik_lapack", "f2py_lapack", "scipy_lapack"}, - } - for library, expected_fixtures in fixture_names.items(): - root = EXAMPLES_ROOT / library - assert (root / "tests" / "helpers.py").is_file() - assert sorted((root / "tests").glob("test_*.py")) - assert {path.name for path in (root / "ci").glob("*.py")} == { - "__init__.py", - "full_surface.py", - } - assert all((root / script).is_file() for script in ("build_prik.sh", "build_f2py.sh", "build_all.sh")) - - aggregate = (root / "build_all.sh").read_text(encoding="utf-8") - assert f"source examples/{library}/build_prik.sh" in aggregate - assert f'source "$EXAMPLE_WORKSPACE/examples/{library}/build_f2py.sh"' in aggregate - - fixture_tree = ast.parse((root / "conftest.py").read_text(encoding="utf-8")) - functions = {node.name for node in fixture_tree.body if isinstance(node, ast.FunctionDef)} - assert functions == expected_fixtures - - -def test_test_index_links_and_language_directories_exist() -> None: - text = TEST_INDEX.read_text(encoding="utf-8") - for target in re.findall(r"\[[^]]+\]\(([^)#]+)", text): - assert (TEST_INDEX.parent / target).resolve().exists(), target - - assert all((TEST_ROOT / language).is_dir() for language in LANGUAGE_DIRECTORIES) - - -def test_maintained_docs_do_not_name_deprecated_pytest_locations() -> None: - stale = [] - for path in _maintained_path_reference_files(): - text = path.read_text(encoding="utf-8") - for pattern in STALE_PYTEST_PATH_PATTERNS: - if pattern.search(text): - stale.append(f"{path.relative_to(REPO_ROOT)}: {pattern.pattern}") - assert stale == [] - - -def test_real_library_examples_have_one_dedicated_workflow() -> None: - ordinary_jobs = TESTS_WORKFLOW.read_text(encoding="utf-8") - dedicated_job = REAL_LIBRARIES_WORKFLOW.read_text(encoding="utf-8") - - assert '-m "not real_library and not toolchain_smoke"' in ordinary_jobs - assert ordinary_jobs.count('-m "not real_library and not toolchain_smoke"') == 2 - assert "examples/blas" not in ordinary_jobs - assert "examples/lapack" not in ordinary_jobs - assert "source examples/blas/build_all.sh" in dedicated_job - assert "source examples/lapack/build_all.sh" in dedicated_job - assert "source examples/fftpack/build_all.sh" in dedicated_job - assert "source examples/minpack/build_all.sh" in dedicated_job - assert f"python -m pytest -q examples/blas/tests {BLAS_CI_FULL_SURFACE}" in dedicated_job - assert f"python -m pytest -q examples/lapack/tests {LAPACK_CI_FULL_SURFACE}" in dedicated_job - assert "python -m pytest -q examples/fftpack/tests" in dedicated_job - assert "python -m pytest -q examples/minpack/tests" in dedicated_job - assert BLAS_CI_FULL_SURFACE in dedicated_job - assert LAPACK_CI_FULL_SURFACE in dedicated_job - assert '"meson==1.11.2"' in dedicated_job - assert '"ninja==1.13.0"' in dedicated_job - assert '"scipy==1.18.0"' in dedicated_job - assert "libblas-dev" in dedicated_job - assert "liblapack-dev" in dedicated_job - assert "ignore-real-library-wrappers" in dedicated_job - assert "matrix.library" not in dedicated_job - - -def test_coverage_workflow_reuses_the_canonical_test_selections() -> None: - ordinary = TESTS_WORKFLOW.read_text(encoding="utf-8") - coverage = COVERAGE_WORKFLOW.read_text(encoding="utf-8") - - for snippet in ( - "tests/architecture", - "tests/c", - "tests/fortran", - "tests/shared", - "-m toolchain_smoke", - '-m "not real_library and not toolchain_smoke"', - "--require-toolchain-smoke", - "--prik-fortran-compiler=gfortran", - ): - assert snippet in ordinary - assert snippet in coverage - assert coverage.count("python -m coverage run -m pytest") == 2 - assert "python -m coverage combine" in coverage - assert "python -m coverage report" in coverage - - -def test_codecov_keeps_project_coverage_blocking_and_patch_coverage_informational() -> None: - assert CODECOV_CONFIG.read_text(encoding="utf-8") == ( - "coverage:\n" - " status:\n" - " project:\n" - " default:\n" - " target: 90%\n" - " patch:\n" - " default:\n" - " target: 90%\n" - " informational: true\n" - ) - - -def test_active_github_action_checks_use_distinct_workflow_scopes_and_job_names() -> None: - workflow_names = { - REAL_LIBRARIES_WORKFLOW: "Real Libraries", - CLAUDE_WORKFLOW: "Repository Automation", - COVERAGE_WORKFLOW: "Quality Metrics", - DOCS_WORKFLOW: "Documentation", - FORTRAN_SMOKE_WORKFLOW: "Compiler Compatibility", - MERGE_VALIDATION_WORKFLOW: "Pull Request", - PUBLISH_WORKFLOW: "Release Automation", - STATIC_ANALYSIS_WORKFLOW: "Code Quality", - TESTS_WORKFLOW: "Test Matrix", - } - expected = { - REAL_LIBRARIES_WORKFLOW: { - "real-library-wrappers": "BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12" - }, - CLAUDE_WORKFLOW: {"claude": "Claude Code response to mention"}, - COVERAGE_WORKFLOW: {"coverage": "Project coverage · Ubuntu 24.04 · Python 3.12"}, - DOCS_WORKFLOW: { - "benchmark": "Documentation performance benchmark · Ubuntu 24.04 ARM64 · Python 3.12", - "build": "Documentation site build · Ubuntu 24.04 · Python 3.12", - "deploy": "Documentation deployment · GitHub Pages", - }, - FORTRAN_SMOKE_WORKFLOW: { - "toolchain-smoke": "Compiler smoke · ${{ matrix.display_name }}", - "macos-flang-smoke": "Compiler smoke · macOS 15 ARM64 · LLVM Flang · Python 3.12", - }, - MERGE_VALIDATION_WORKFLOW: { - "static-analysis": "Static analysis · Ubuntu 24.04 · Python 3.12", - "compiler-smoke": "Compiler smoke · ${{ matrix.display_name }}", - "compiler-smoke-macos": "Compiler smoke · macOS 15 ARM64 · LLVM Flang · Python 3.12", - "unit-tests": "${{ matrix.display_name }}", - "unit-tests-macos": "Unit tests · macOS 15 ARM64 · Python 3.12", - "native-libraries": "BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12", - "documentation-benchmark": "Documentation performance benchmark · Ubuntu 24.04 ARM64 · Python 3.12", - "documentation-build": "Documentation site build · Ubuntu 24.04 · Python 3.12", - "merge-gate": "Validation · all required checks", - }, - PUBLISH_WORKFLOW: { - "build": "PyPI distribution build · Ubuntu 24.04 · Python 3.12", - "publish": "PyPI trusted publishing · pypi", - }, - STATIC_ANALYSIS_WORKFLOW: {"static-analysis": "Static analysis · Ubuntu 24.04 · Python 3.12"}, - TESTS_WORKFLOW: { - "test": "Unit tests · Ubuntu 24.04 · Python ${{ matrix.python-version }}", - "macos": "Unit tests · macOS 15 ARM64 · Python 3.12", - }, - } - - assert set(workflow_names) == set(expected) == set(WORKFLOW_ROOT.glob("*.yml")) - assert {workflow: _github_action_workflow_name(workflow) for workflow in workflow_names} == workflow_names - assert len(workflow_names.values()) == len(set(workflow_names.values())) - assert {workflow: _github_action_job_display_names(workflow) for workflow in expected} == expected - - display_names = [name for jobs in expected.values() for name in jobs.values()] - check_contexts = [ - f"{workflow_names[workflow]} / {job_name}" for workflow, jobs in expected.items() for job_name in jobs.values() - ] - assert len(check_contexts) == len(set(check_contexts)) - assert all(not re.fullmatch(r"Python(?:\s+.*)?", name) for name in display_names) - - smoke_matrix_names = _github_matrix_display_names(FORTRAN_SMOKE_WORKFLOW) - assert smoke_matrix_names == ( - "Ubuntu 24.04 · Intel IFX 2026.1.1 · Python 3.12", - "Ubuntu 24.04 · LLVM Flang 22.1.8 · Python 3.12", - ) - assert 'python-version: ["3.10", "3.11", "3.12"]' in TESTS_WORKFLOW.read_text(encoding="utf-8") - documented_contexts = ("Pull Request / Validation · all required checks",) - quality_assurance = QUALITY_ASSURANCE_DOC.read_text(encoding="utf-8") - assert len(documented_contexts) == len(set(documented_contexts)) - for context in documented_contexts: - assert f"`{context}`" in quality_assurance - - -def test_pull_request_declares_direct_staged_jobs_and_always_reports_the_gate() -> None: - workflow = MERGE_VALIDATION_WORKFLOW.read_text(encoding="utf-8") - - assert workflow.startswith("name: Pull Request\n") - assert " pull_request:\n types: [opened, synchronize, reopened, labeled, unlabeled]" in workflow - assert "group: pull-request-${{ github.ref }}" in workflow - assert "cancel-in-progress: true" in workflow - assert "uses: ./.github/workflows/" not in workflow - - static_analysis = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, "static-analysis") - assert "needs:" not in static_analysis - assert "runs-on:" in static_analysis - - for job_id in ("compiler-smoke", "compiler-smoke-macos"): - block = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, job_id) - assert "needs: static-analysis" in block - assert "runs-on:" in block - - for job_id in ("unit-tests", "unit-tests-macos"): - block = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, job_id) - assert "needs: [compiler-smoke, compiler-smoke-macos]" in block - assert "runs-on:" in block - - unit_tests = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, "unit-tests") - assert "Unit tests + project coverage · Ubuntu 24.04 · Python 3.12" in unit_tests - assert "python -m coverage combine" in unit_tests - assert "python -m coverage report" in unit_tests - assert "uses: codecov/codecov-action@v6" in unit_tests - assert "id-token: write" in unit_tests - - native_libraries = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, "native-libraries") - assert "needs: [unit-tests, unit-tests-macos]" in native_libraries - assert "ignore-real-library-wrappers" in native_libraries - assert "source examples/blas/build_all.sh" in native_libraries - assert "source examples/lapack/build_all.sh" in native_libraries - assert f"python -m pytest -q examples/blas/tests {BLAS_CI_FULL_SURFACE}" in native_libraries - assert f"python -m pytest -q examples/lapack/tests {LAPACK_CI_FULL_SURFACE}" in native_libraries - assert BLAS_CI_FULL_SURFACE in native_libraries - assert LAPACK_CI_FULL_SURFACE in native_libraries - - benchmark = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, "documentation-benchmark") - assert "needs: native-libraries" in benchmark - assert "always()" in benchmark - assert "bash benchmarks/run.sh" in benchmark - - documentation = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, "documentation-build") - assert "needs: documentation-benchmark" in documentation - assert "python -m pytest -q tests/shared/docs" in documentation - assert "python -m mkdocs build --strict" in documentation - - gate = _github_action_job_block(MERGE_VALIDATION_WORKFLOW, "merge-gate") - assert "if: ${{ always() }}" in gate - for dependency in ( - "static-analysis", - "compiler-smoke", - "compiler-smoke-macos", - "unit-tests", - "unit-tests-macos", - "native-libraries", - "documentation-benchmark", - "documentation-build", - ): - assert f" - {dependency}" in gate - assert f"needs.{dependency}.result" in gate - assert 'if [[ "$result" != "success" ]]' in gate - assert 'exit "$failed"' in gate - - -def test_purpose_specific_workflows_do_not_create_duplicate_pull_request_runs() -> None: - for workflow in ( - TESTS_WORKFLOW, - STATIC_ANALYSIS_WORKFLOW, - REAL_LIBRARIES_WORKFLOW, - FORTRAN_SMOKE_WORKFLOW, - COVERAGE_WORKFLOW, - DOCS_WORKFLOW, - ): - text = workflow.read_text(encoding="utf-8") - assert " workflow_call:" not in text - assert " pull_request:" not in text - - assert "run-coverage" not in COVERAGE_WORKFLOW.read_text(encoding="utf-8") - - -def test_pull_request_jobs_share_the_reviewed_component_workflow_steps() -> None: - for pull_request_job, component_workflow, component_job in ( - ("static-analysis", STATIC_ANALYSIS_WORKFLOW, "static-analysis"), - ("compiler-smoke", FORTRAN_SMOKE_WORKFLOW, "toolchain-smoke"), - ("compiler-smoke-macos", FORTRAN_SMOKE_WORKFLOW, "macos-flang-smoke"), - ("unit-tests-macos", TESTS_WORKFLOW, "macos"), - ("native-libraries", REAL_LIBRARIES_WORKFLOW, "real-library-wrappers"), - ("documentation-benchmark", DOCS_WORKFLOW, "benchmark"), - ): - assert _github_action_job_steps(MERGE_VALIDATION_WORKFLOW, pull_request_job) == _github_action_job_steps( - component_workflow, component_job - ) - - pull_request_build = _github_action_job_steps(MERGE_VALIDATION_WORKFLOW, "documentation-build") - main_build = _github_action_job_steps(DOCS_WORKFLOW, "build") - configure_pages = main_build.index(" - name: Configure GitHub Pages") - assert pull_request_build == main_build[:configure_pages] - - -def test_pypi_package_identity_is_complete_and_consistent() -> None: - pyproject = PYPROJECT.read_text(encoding="utf-8") - changelog = CHANGELOG.read_text(encoding="utf-8") - root_readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") - website_home = (REPO_ROOT / "docs/index.md").read_text(encoding="utf-8") - - for declaration in ( - 'name = "prik"', - 'version = "0.1.1"', - '"Development Status :: 3 - Alpha"', - 'prik = "prik.cli:main"', - 'Homepage = "https://pynumlab.github.io/prik/"', - 'Repository = "https://github.com/PyNumLab/prik"', - 'Issues = "https://github.com/PyNumLab/prik/issues"', - 'Changelog = "https://github.com/PyNumLab/prik/blob/main/CHANGELOG.md"', - ): - assert declaration in pyproject - assert "## 0.1.1 — 2026-08-03" in changelog - assert "`prik --version`" in changelog - assert "`prik.__version__`" in changelog - assert "**Project status: Alpha (`0.1.x`).**" in root_readme - assert "**Project status: Alpha (`0.1.x`).**" in website_home - assert MANIFEST.read_text(encoding="utf-8") == "include CHANGELOG.md\n" - - -def test_pypi_publication_uses_a_protected_token_free_release_job() -> None: - workflow = PUBLISH_WORKFLOW.read_text(encoding="utf-8") - build_job, publish_job = workflow.split(" publish:\n", maxsplit=1) - - for declaration in ( - " release:\n types: [published]", - "persist-credentials: false", - "release tag {release_tag!r} must be {expected_tag!r}", - "python -m build", - "python -m twine check dist/*", - 'compgen -G "dist/prik-*-py3-none-any.whl"', - 'bin/prik" --version', - "prik.__version__ == m.version", - "uses: actions/upload-artifact@v4", - ): - assert declaration in build_job - - assert "id-token: write" not in build_job - assert "needs: build" in publish_job - assert "name: pypi" in publish_job - assert "id-token: write" in publish_job - assert "uses: actions/download-artifact@v4" in publish_job - assert "uses: pypa/gh-action-pypi-publish@release/v1" in publish_job - assert "secrets." not in workflow - assert "password:" not in workflow - assert "workflow_dispatch:" not in workflow - - -def test_static_analysis_targets_the_renamed_package_tree() -> None: - workflow = STATIC_ANALYSIS_WORKFLOW.read_text(encoding="utf-8") - - assert "python -m bandit -c pyproject.toml -r prik" in workflow - assert "python -m radon cc prik" in workflow - assert "python -m radon mi prik" in workflow - for removed_root in ("c_parser", "fortran_parser", "semantics"): - assert removed_root not in workflow - - -def test_pull_request_and_main_generate_equivalent_documentation_performance_snapshots() -> None: - pull_request = MERGE_VALIDATION_WORKFLOW.read_text(encoding="utf-8") - main = DOCS_WORKFLOW.read_text(encoding="utf-8") - - assert "group: documentation-${{ github.ref }}" in main - assert "cancel-in-progress: true" in main - assert " workflow_call:" not in main - for declaration in ( - "runs-on: ubuntu-24.04-arm", - "python tools/benchmark_host.py", - "--require-machine aarch64", - "--require-arm-part 0xd49", - '--github-env "$GITHUB_ENV"', - "bash benchmarks/run.sh", - "python tools/generate_performance_docs.py", - "name: performance-snapshot", - "benchmarks/results/f2py.json", - "benchmarks/results/prik.json", - "benchmarks/results/f2py-prik-first.json", - "benchmarks/results/f2py-f2py-first.json", - "benchmarks/results/prik-prik-first.json", - "benchmarks/results/prik-f2py-first.json", - "benchmarks/results/f2py-build.json", - "benchmarks/results/prik-build.json", - "uses: actions/download-artifact@v4", - "python -m pytest -q tests/shared/docs", - "python -m mkdocs build --strict", - ): - assert declaration in pull_request - assert declaration in main - - assert main.count("github.ref == 'refs/heads/main' && github.event_name != 'pull_request'") == 3 diff --git a/tests/shared/docs/test_structure.py b/tests/shared/docs/test_structure.py deleted file mode 100644 index 62d3043de..000000000 --- a/tests/shared/docs/test_structure.py +++ /dev/null @@ -1,1674 +0,0 @@ -"""Verify the documentation architecture contract.""" - -from __future__ import annotations - -from functools import cache -from pathlib import Path -import re -import subprocess -import sys - -import pytest -import prik - - -ROOT = Path(__file__).parents[3] -DOCS_ROOT = ROOT / "docs" -FEATURE_MATRIX_PATH = DOCS_ROOT / "user/language-support/feature-matrix.md" -CLI_REFERENCE_PATH = DOCS_ROOT / "user/reference/cli-commands.md" -PYTHON_API_REFERENCE_PATH = DOCS_ROOT / "user/reference/python-api.md" -DOCUMENTATION_CHECKLIST_PATH = DOCS_ROOT / "maintainer/roadmap/documentation-content-checklist.md" -DOC_PATHS = sorted(path for path in DOCS_ROOT.rglob("*.md") if "old_docs" not in path.parts) -WEBSITE_DOCUMENTATION_PATHS = [ - DOCS_ROOT / "index.md", - *sorted((DOCS_ROOT / "user").rglob("*.md")), - *sorted((DOCS_ROOT / "developer").rglob("*.md")), - *sorted((DOCS_ROOT / "maintainer").rglob("*.md")), -] -LEARNING_DOCUMENTATION_PATHS = [ - *sorted((DOCS_ROOT / "user").rglob("*.md")), - *sorted((DOCS_ROOT / "developer").rglob("*.md")), -] -DEFERRED_C_PAGE_PATHS = [ - ROOT / "docs/maintainer/design/cpython-integration.md", - ROOT / "docs/developer/c-parser-reference.md", - ROOT / "docs/user/examples/recipes/inspect-c-api.md", -] -MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)#]+)(?:#[^)]+)?\)") -NEXT_NAVIGATION = re.compile(r"^\s*(?:#{2,6}\s+Next|\*\*Next\*\*:?)\s*$", re.IGNORECASE) -NEXT_SECTION_BOUNDARY = re.compile(r"^\s*(?:#{2,6}\s+|---\s*$|\*\*[^*]+\*\*)") -ALLOWED_CONTEXTUAL_FORWARD_LINK_SOURCE_PREFIXES = ("user/getting-started/", "user/guide/") -ALLOWED_CONTEXTUAL_FORWARD_LINK_PREFIXES = ("user/reference/pyi-contracts/",) -C_DOCS_START = "" -C_DOCS_DISABLED = "" and hidden == "ordinary": - hidden = None - elif hidden == "deferred-c": - assert "--" not in line, f"{path.relative_to(ROOT)}: invalid double hyphen in deferred comment" - elif hidden == "ordinary": - continue - elif not line.lstrip().startswith(C_DOCS_DISABLED): - visible.append(line) - assert not hidden, f"{path.relative_to(ROOT)}: unclosed deferred documentation comment" - return "\n".join(visible) - - -def _instructional_body_without_next(body: str) -> str: - instructional_lines: list[str] = [] - inside_next = False - - for line in body.splitlines(): - if NEXT_NAVIGATION.match(line): - inside_next = True - continue - if inside_next and NEXT_SECTION_BOUNDARY.match(line): - inside_next = False - if not inside_next: - instructional_lines.append(line) - - return "\n".join(instructional_lines) - - -def _next_navigation_items(body: str) -> list[tuple[int, str, bool]]: - items: list[tuple[int, str, bool]] = [] - current_line: int | None = None - current_parts: list[str] = [] - inside_next = False - - def flush_current() -> None: - nonlocal current_line, current_parts - if current_line is not None: - items.append((current_line, " ".join(current_parts), True)) - current_line = None - current_parts = [] - - for line_number, line in enumerate(body.splitlines(), start=1): - if NEXT_NAVIGATION.match(line): - flush_current() - inside_next = True - continue - if inside_next and NEXT_SECTION_BOUNDARY.match(line): - flush_current() - inside_next = False - continue - if not inside_next or not line.strip(): - continue - if line.startswith("- "): - flush_current() - current_line = line_number - current_parts = [line[2:].strip()] - elif current_line is not None and line.startswith(" "): - current_parts.append(line.strip()) - else: - flush_current() - items.append((line_number, line.strip(), False)) - - flush_current() - return items - - -def _combined_text(relative_paths: list[str]) -> str: - return "\n".join((ROOT / relative_path).read_text(encoding="utf-8") for relative_path in relative_paths) - - -@cache -def _site_navigation_positions() -> dict[str, int]: - navigation_entry = re.compile(r": ([^#\s]+\.md)\s*$") - paths: list[str] = [] - for line in (ROOT / "mkdocs.yml").read_text(encoding="utf-8").splitlines(): - if line.lstrip().startswith("#"): - continue - match = navigation_entry.search(line) - if match: - paths.append(match.group(1)) - return {path: index for index, path in enumerate(paths)} - - -def _user_guide_index_order() -> list[str]: - _, body = _front_matter(DOCS_ROOT / "user/guide/index.md") - guide_root = (DOCS_ROOT / "user/guide").resolve() - paths: list[str] = [] - for target in MARKDOWN_LINK.findall(body): - resolved = (guide_root / target).resolve() - if resolved.parent != guide_root or resolved.name == "index.md": - continue - relative_path = resolved.relative_to(DOCS_ROOT).as_posix() - if relative_path not in paths: - paths.append(relative_path) - return paths - - -@cache -def _prik_cli_help() -> str: - commands = [ - ["--help"], - ["input.f90", "--help"], - ["parse", "--help"], - ["semantics", "--help"], - ["generate", "--help"], - ["probe", "--help"], - ] - outputs = [] - for command in commands: - result = subprocess.run( - [sys.executable, "-m", "prik", *command], - cwd=ROOT, - capture_output=True, - text=True, - check=True, - ) - outputs.append(result.stdout) - return "\n".join(outputs) - - -def _feature_matrix_rows() -> list[dict[str, str]]: - header = "| Feature | Status | User docs | Source owner | Evidence | Limitations |" - columns = ["Feature", "Status", "User docs", "Source owner", "Evidence", "Limitations"] - rows: list[dict[str, str]] = [] - in_table = False - - for line in FEATURE_MATRIX_PATH.read_text(encoding="utf-8").splitlines(): - if line == header: - in_table = True - continue - if not in_table: - continue - if line.startswith("| ---"): - continue - if not line.startswith("|"): - in_table = False - continue - - cells = [cell.strip() for cell in line.strip().strip("|").split("|")] - assert len(cells) == len(columns), f"invalid feature matrix row: {line!r}" - rows.append(dict(zip(columns, cells, strict=True))) - - return rows - - -FEATURE_MATRIX_ROWS = _feature_matrix_rows() - - -@pytest.mark.parametrize("path", DOC_PATHS, ids=lambda path: str(path.relative_to(ROOT))) -def test_documentation_page_metadata(path: Path) -> None: - metadata, body = _front_matter(path) - missing = REQUIRED_METADATA - metadata.keys() - assert not missing, f"{path.relative_to(ROOT)}: missing metadata fields: {sorted(missing)}" - - for key in REQUIRED_METADATA: - assert metadata[key], f"{path.relative_to(ROOT)}: metadata field {key!r} is empty" - - assert metadata["status"] in ALLOWED_STATUSES, f"{path.relative_to(ROOT)}: unknown status {metadata['status']!r}" - assert metadata["publication"] in ALLOWED_PUBLICATION_STATES, ( - f"{path.relative_to(ROOT)}: unknown publication state {metadata['publication']!r}" - ) - if metadata["status"] in TODO_STATUSES: - assert "## TODO" in body, f"{path.relative_to(ROOT)}: unfinished pages must include a TODO section" - assert "TODO:" in body, f"{path.relative_to(ROOT)}: TODO section must contain explicit TODO markers" - - -@pytest.mark.parametrize( - "path", - [ - ROOT / "README.md", - *(path for path in WEBSITE_DOCUMENTATION_PATHS if _front_matter(path)[0].get("publication") == "reviewed"), - ], - ids=lambda path: str(path.relative_to(ROOT)), -) -def test_deferred_c_documentation_is_not_visible(path: Path) -> None: - visible = _visible_documentation_source(path) - for allowed_text in VISIBLE_C_DOCUMENTATION_EXCEPTIONS.get(str(path.relative_to(ROOT)), ()): - visible = visible.replace(allowed_text, "") - match = VISIBLE_C_DOCUMENTATION.search(visible) - assert match is None, f"{path.relative_to(ROOT)}: visible deferred documentation: {match.group(0)!r}" - - -@pytest.mark.parametrize("path", DEFERRED_C_PAGE_PATHS, ids=lambda path: str(path.relative_to(ROOT))) -def test_dedicated_deferred_c_pages_have_no_visible_body(path: Path) -> None: - assert _visible_documentation_source(path).strip() == "" - - -def test_deferred_c_pages_are_not_in_site_navigation() -> None: - lines = (ROOT / "mkdocs.yml").read_text(encoding="utf-8").splitlines() - active_navigation = "\n".join(line for line in lines if not line.lstrip().startswith("#")) - assert "Inspect a C API" not in active_navigation - assert "C Parser Reference" not in active_navigation - assert any("PRIK_C_DOCS" in line and "inspect-c-api.md" in line for line in lines) - assert any("PRIK_C_DOCS" in line and "c-parser-reference.md" in line for line in lines) - - -def test_readme_follows_one_points_workflow_from_build_through_contract_rebuild() -> None: - readme = _visible_documentation_source(ROOT / "README.md") - quick_start = readme.split("## Installation & Quick Start", maxsplit=1)[1].split( - "## How it works", - maxsplit=1, - )[0] - installation_index = quick_start.index("python3 -m pip install prik") - version_index = quick_start.index("prik --version", installation_index) - help_index = quick_start.index("python3 -m prik --help") - source_build_command_index = quick_start.index( - "python3 -m prik points.f90 --out geometry", - help_index, - ) - source_build_tree_index = quick_start.index( - ".\n points.f90\n geometry.so\n __prik__/", - source_build_command_index, - ) - explicit_source_build_command_index = quick_start.index( - "python3 -m prik points.f90 \\\n --out geometry \\\n --out-dir build/geometry", - source_build_tree_index, - ) - explicit_source_build_tree_index = quick_start.index( - "build/geometry/\n geometry..so", - explicit_source_build_command_index, - ) - pyi_generation_command_index = quick_start.index( - "python3 -m prik generate --pyi points.f90 --out contracts", - explicit_source_build_tree_index, - ) - pyi_contract_tree_index = quick_start.index( - "contracts/\n __init__.pyi\n points.pyi", - pyi_generation_command_index, - ) - pyi_contract_body_index = quick_start.index( - "class point:\n" - " def __init__(\n" - " self,\n" - " *,\n" - " x: Float64 = 0.0,\n" - " y: Float64 = 0.0", - pyi_contract_tree_index, - ) - move_contract_index = quick_start.index( - "@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))])\ndef move(", - pyi_contract_body_index, - ) - norm_contract_index = quick_start.index("def norm_squared(", move_contract_index) - pyi_build_command_index = quick_start.index( - "python3 -m prik contracts/__init__.pyi", - norm_contract_index, - ) - native_source_argument_index = quick_start.index("--native-fortran-sources points.f90", pyi_build_command_index) - output_name_index = quick_start.index("--out geometry", native_source_argument_index) - pyi_build_tree_index = quick_start.index("build/geometry_from_pyi/", output_name_index) - import_index = quick_start.index("import geometry.points as points", pyi_build_tree_index) - constructor_index = quick_start.index("item = points.point(", import_index) - mutation_index = quick_start.index("points.move(item", constructor_index) - runtime_output_index = quick_start.index("# 20.0", mutation_index) - verbose_command_index = quick_start.index( - "python3 -m prik points.f90 \\\n --out geometry_debug", - runtime_output_index, - ) - verbose_fortran_flag_index = quick_start.index("--wrapper-fortran-flags=-O2", verbose_command_index) - verbose_c_flag_index = quick_start.index("--wrapper-c-flags=-O2", verbose_fortran_flag_index) - verbose_output_index = quick_start.index("generated Python binding", verbose_c_flag_index) - - assert installation_index < version_index < help_index < source_build_command_index - assert source_build_command_index < source_build_tree_index < explicit_source_build_command_index - assert explicit_source_build_command_index < explicit_source_build_tree_index < pyi_generation_command_index - assert pyi_generation_command_index < pyi_contract_tree_index < pyi_contract_body_index - assert pyi_contract_body_index < move_contract_index < norm_contract_index < pyi_build_command_index - assert pyi_build_command_index < native_source_argument_index < output_name_index < pyi_build_tree_index - assert pyi_build_tree_index < import_index < constructor_index < mutation_index < runtime_output_index - assert runtime_output_index < verbose_command_index - assert verbose_command_index < verbose_fortran_flag_index < verbose_c_flag_index < verbose_output_index - assert "--parse" not in readme - assert "--semantics" not in readme - assert "scale.f90" not in readme - assert "SCALE" not in readme - assert "python3 -m prik solver.f90" not in quick_start - assert "fruntime_abi_f90" not in readme - assert "solver.f90" not in readme - assert "add1" not in readme - assert "distance2" not in readme - assert "point_api" not in readme - assert "build/points" not in readme - assert "tests/data/fortran/general/basic_subroutine.f90" not in readme - assert "contracts/basic_subroutine/basic_subroutine.pyi" not in readme - - -def test_installation_separates_pypi_users_from_editable_contributors() -> None: - installation = _visible_documentation_source(DOCS_ROOT / "user/getting-started/installation.md") - user_section = installation.split("## User Installation", maxsplit=1)[1].split( - "## Contributor Installation", - maxsplit=1, - )[0] - contributor_section = installation.split("## Contributor Installation", maxsplit=1)[1].split( - "## Platform Support", - maxsplit=1, - )[0] - - assert "python3 -m pip install prik" in user_section - assert "pip install -e" not in user_section - assert "git clone https://github.com/PyNumLab/prik.git" in contributor_section - assert 'python3 -m pip install -e ".[qa]"' in contributor_section - - -@pytest.mark.parametrize("relative_path", REQUIRED_AREA_INDEXES) -def test_required_documentation_area_exists(relative_path: str) -> None: - assert (DOCS_ROOT / relative_path).is_file() - - -def test_documentation_root_uses_three_audience_lanes() -> None: - directories = {path.name for path in DOCS_ROOT.iterdir() if path.is_dir()} - root_pages = {path.name for path in DOCS_ROOT.glob("*.md")} - assert directories == { - "user", - "developer", - "maintainer", - "javascripts", - "stylesheets", - "old_docs", - } - assert root_pages == {"index.md"} - - -@pytest.mark.parametrize( - ("lane", "audience_terms"), - [ - ("user", ("users",)), - ("developer", ("developers", "contributors")), - ("maintainer", ("maintainers",)), - ], -) -def test_documentation_lane_has_consistent_audience(lane: str, audience_terms: tuple[str, ...]) -> None: - for path in (DOCS_ROOT / lane).rglob("*.md"): - metadata, _ = _front_matter(path) - assert any(term in metadata["audience"] for term in audience_terms) - if lane != "maintainer": - assert "maintainers" not in metadata["audience"] - else: - assert metadata["audience"] == "maintainers" - - -@pytest.mark.parametrize("path", LEARNING_DOCUMENTATION_PATHS, ids=lambda path: str(path.relative_to(ROOT))) -def test_website_documentation_does_not_link_to_maintainer_lane(path: Path) -> None: - maintainer_root = (DOCS_ROOT / "maintainer").resolve() - for target in MARKDOWN_LINK.findall(_visible_documentation_source(path)): - if target.startswith(("http://", "https://", "mailto:")): - continue - resolved = (path.parent / target).resolve() - assert not resolved.is_relative_to(maintainer_root), ( - f"{path.relative_to(ROOT)}: website link enters maintainer lane: {target}" - ) - - -def test_readme_documentation_links_follow_site_navigation_order() -> None: - readme = _visible_documentation_source(ROOT / "README.md") - documentation = readme.split("## Documentation", maxsplit=1)[1].split( - "## Development", - maxsplit=1, - )[0] - positions = _site_navigation_positions() - linked_positions = [ - positions[target.removeprefix("docs/")] - for target in MARKDOWN_LINK.findall(documentation) - if target.startswith("docs/") - ] - assert linked_positions == sorted(linked_positions) - - -@pytest.mark.parametrize("relative_path", REQUIRED_REFERENCE_PAGES) -def test_required_reference_page_exists(relative_path: str) -> None: - assert (DOCS_ROOT / relative_path).is_file() - - -@pytest.mark.parametrize("relative_path", REQUIRED_REFERENCE_PAGES) -def test_reference_page_is_in_site_navigation(relative_path: str) -> None: - site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - assert relative_path in site_configuration - - -@pytest.mark.parametrize("relative_path", REQUIRED_ROADMAP_PAGES) -def test_required_roadmap_page_exists(relative_path: str) -> None: - assert (DOCS_ROOT / relative_path).is_file() - - -def test_site_navigation_includes_all_publishable_lanes_and_excludes_archive() -> None: - site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - assert "old_docs/**" in site_configuration - positions = _site_navigation_positions() - assert "user/index.md" in positions - assert "developer/index.md" in positions - assert "maintainer/README.md" in positions - - -def test_user_guide_navigation_follows_index_reading_order() -> None: - positions = _site_navigation_positions() - navigation_order = [ - path - for path, _ in sorted(positions.items(), key=lambda item: item[1]) - if path.startswith("user/guide/") and path != "user/guide/index.md" - ] - - assert navigation_order == _user_guide_index_order() - - -@pytest.mark.parametrize("relative_path", REQUIRED_GETTING_STARTED_PAGES) -def test_required_getting_started_page_is_maintained_and_navigable(relative_path: str) -> None: - path = DOCS_ROOT / relative_path - assert path.is_file() - metadata, body = _front_matter(path) - assert metadata["status"] == "maintained" - assert relative_path in (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - for target in MARKDOWN_LINK.findall(body): - if target.startswith(("http://", "https://")): - continue - assert (path.parent / target).resolve().exists(), f"{relative_path}: missing link target {target}" - - -@pytest.mark.parametrize("relative_path", REQUIRED_GETTING_STARTED_PAGES) -def test_getting_started_page_is_completed_in_documentation_checklist(relative_path: str) -> None: - checklist = DOCUMENTATION_CHECKLIST_PATH.read_text(encoding="utf-8") - assert f"- [x] `docs/{relative_path}`" in checklist - - -@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) -def test_required_user_guide_page_is_maintained_and_navigable(relative_path: str) -> None: - path = DOCS_ROOT / relative_path - assert path.is_file() - metadata, body = _front_matter(path) - assert metadata["status"] == "maintained" - assert relative_path in (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - for target in MARKDOWN_LINK.findall(body): - if target.startswith(("http://", "https://")): - continue - assert (path.parent / target).resolve().exists(), f"{relative_path}: missing link target {target}" - - -@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) -def test_user_guide_page_is_completed_in_documentation_checklist(relative_path: str) -> None: - checklist = DOCUMENTATION_CHECKLIST_PATH.read_text(encoding="utf-8") - assert f"- [x] `docs/{relative_path}`" in checklist - - -@pytest.mark.parametrize( - "relative_path", - [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:], *EXAMPLE_DOCUMENTATION_PAGES], -) -def test_sequential_user_pages_do_not_link_forward_from_instructional_prose(relative_path: str) -> None: - path = DOCS_ROOT / relative_path - _, body = _front_matter(path) - body = _instructional_body_without_next(body) - positions = _site_navigation_positions() - if relative_path not in positions: - pytest.skip(f"{relative_path}: not active in site navigation") - source_position = positions[relative_path] - for target in MARKDOWN_LINK.findall(body): - target_path = (path.parent / target).resolve() - if not target_path.is_relative_to(DOCS_ROOT): - continue - target_relative = target_path.relative_to(DOCS_ROOT).as_posix() - if target_relative not in positions: - continue - if relative_path.startswith(ALLOWED_CONTEXTUAL_FORWARD_LINK_SOURCE_PREFIXES) and target_relative.startswith( - ALLOWED_CONTEXTUAL_FORWARD_LINK_PREFIXES - ): - continue - assert positions[target_relative] <= source_position, f"{relative_path}: forward link to {target_relative}" - - -@pytest.mark.parametrize( - "relative_path", - [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:]], -) -def test_next_sections_use_linked_bullet_destinations(relative_path: str) -> None: - _, body = _front_matter(DOCS_ROOT / relative_path) - for line_number, item, is_bullet in _next_navigation_items(body): - assert is_bullet, f"{relative_path}:{line_number}: Next content must be a bullet item" - assert MARKDOWN_LINK.search(item), f"{relative_path}:{line_number}: Next item must include a Markdown link" - - -@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) -def test_user_guide_commands_do_not_expose_fixture_paths(relative_path: str) -> None: - page = (DOCS_ROOT / relative_path).read_text(encoding="utf-8") - assert "python3 -m prik tests/" not in page - - -@pytest.mark.parametrize( - "relative_path", - [ - "index.md", - "user/index.md", - *REQUIRED_GETTING_STARTED_PAGES, - *REQUIRED_USER_GUIDE_PAGES, - ], -) -def test_reviewed_user_pages_do_not_expose_internal_evidence(relative_path: str) -> None: - page = _visible_documentation_source(DOCS_ROOT / relative_path) - assert "## Evidence" not in page - assert "## Runtime Evidence" not in page - assert "Runtime tests:" not in page - assert "../../../tests/" not in page - assert "../../tests/" not in page - assert "../tests/" not in page - - -@pytest.mark.parametrize( - "relative_path", - [ - "index.md", - "user/index.md", - *REQUIRED_GETTING_STARTED_PAGES, - *REQUIRED_USER_GUIDE_PAGES, - ], -) -def test_reviewed_user_pages_do_not_contain_editorial_notes(relative_path: str) -> None: - page = _visible_documentation_source(DOCS_ROOT / relative_path).casefold() - for phrase in ( - "i kept your", - "let me know if you want", - "original content had", - "restore/polish", - ): - assert phrase not in page - - -@pytest.mark.parametrize("relative_path", REAL_LIBRARY_EXAMPLE_PAGES) -def test_real_library_examples_share_a_user_facing_structure(relative_path: str) -> None: - page = _visible_documentation_source(DOCS_ROOT / relative_path) - common_sections = [ - "### What this example shows", - "## Versions used", - "## 1. Prepare the repository and toolchain", - "## 4. Run the complete test suite", - "## 5. See how results are validated", - "## 6. Run focused examples", - "## Troubleshooting", - "## Source provenance", - ] - - positions = [page.index(section) for section in common_sections] - assert positions == sorted(positions) - for internal_phrase in ( - "stopping after a successful import", - "fail-closed", - "authoritative public classification", - "complete maintained suite", - "machine constants", - ): - assert internal_phrase not in page - - -def test_getting_started_overview_uses_standalone_example() -> None: - overview = (DOCS_ROOT / "user/getting-started/index.md").read_text(encoding="utf-8") - introduction_index = overview.index("you will create\n`scale.f90`") - import_index = overview.index("import scale") - call_index = overview.index("scale.scale(np.float64(3.0), np.float64(2.5))") - - assert introduction_index < import_index < call_index - assert "[Your First Function](first-wrapped-function.md)" in overview - - -def test_documentation_homepage_demonstrates_prik_before_getting_started() -> None: - page = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") - introduction_index = page.index("Generate native Python bindings for Fortran") - example_heading_index = page.index("## From Fortran to Python in one command") - source_index = page.index("```fortran", example_heading_index) - build_index = page.index("python3 -m prik scale.f90", source_index) - import_index = page.index("import scale", build_index) - call_index = page.index("scale.scale(np.float64(3.0), np.float64(2.5))", import_index) - result_index = page.index("# 7.5", call_index) - advantages_index = page.index("## Why PRIK", result_index) - evidence_index = page.index("## Proven on real Fortran libraries", advantages_index) - performance_index = page.index("## Measured against NumPy's f2py", evidence_index) - runtime_chart_index = page.index("user/assets/performance-comparison.svg", performance_index) - build_chart_index = page.index("user/assets/build-time-comparison.svg", runtime_chart_index) - methodology_index = page.index("[See the benchmark machine, full results, and methodology →]", build_chart_index) - install_index = page.index("[Install PRIK →]", methodology_index) - getting_started_index = page.index("[Read Getting Started →]", install_index) - - assert introduction_index < example_heading_index < source_index < build_index - assert build_index < import_index < call_index < result_index - assert result_index < advantages_index < evidence_index < performance_index - assert performance_index < runtime_chart_index < build_chart_index - assert build_chart_index < methodology_index < install_index < getting_started_index - assert "python3 -m pip install prik" in page - assert "No manual binding code is required." in page - assert "[BLAS](user/examples/blas-wrapper.md)" in page - assert "[LAPACK](user/examples/lapack-wrapper.md)" in page - assert "[FFTPACK](user/examples/fftpack-wrapper.md)" in page - assert "[MINPACK](user/examples/minpack-wrapper.md)" in page - assert "The charts show the current published snapshot" in page - assert "specific to its machine and toolchain" in page - assert "PRIK was faster in" not in page - assert page.count("{ .prik-performance-chart }") == 2 - assert page.count("{ .prik-primary-cta }") == 2 - assert "developer/index.md" not in page - assert "maintainer/README.md" not in page - assert "user/guide/" not in page - - -def test_public_entrypoints_use_the_canonical_identity_and_description() -> None: - homepage = _visible_documentation_source(DOCS_ROOT / "index.md") - readme = _visible_documentation_source(ROOT / "README.md") - mkdocs = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") - title = "PRIK — Python Runtime Interop Kit" - subtitle = "Generate native Python bindings for Fortran, with editable `.pyi` contracts\nand Pythonic APIs." - description = ( - "PRIK generates native Python bindings from Fortran projects, producing\n" - "importable extensions and editable `.pyi` contracts for Pythonic APIs." - ) - language_direction = ( - "**PRIK starts with Fortran-to-Python.** Its semantic contract model is designed\n" - "to support more native languages over time." - ) - metadata_description = description.replace("\n", " ").replace("`", "") - - assert readme.startswith(f"# {title}\n\n**{subtitle}**\n\n{description}\n") - assert f"# {title}\n\n**{subtitle}**\n\n{description}\n" in homepage - assert language_direction in readme - assert language_direction in homepage - assert f"site_name: {title}" in mkdocs - assert f"site_description: {metadata_description}" in mkdocs - assert f'description = "{metadata_description}"' in pyproject - - -def test_faq_routes_search_questions_to_authoritative_pages() -> None: - metadata, page = _front_matter(DOCS_ROOT / "user/faq/index.md") - question_targets = [ - ( - "How do I call Fortran from Python?", - "how-do-i-call-fortran-from-python", - "../getting-started/first-wrapped-function.md", - ), - ( - "How do I generate Python bindings for a Fortran module?", - "how-do-i-generate-python-bindings-for-a-fortran-module", - "../getting-started/first-wrapped-module.md", - ), - ( - "How do I wrap an existing Fortran library for Python?", - "how-do-i-wrap-an-existing-fortran-library-for-python", - "../guide/building-shared-library.md", - ), - ( - "How do I expose Fortran derived types as Python classes?", - "how-do-i-expose-fortran-derived-types-as-python-classes", - "../guide/wrapping-derived-types.md", - ), - ( - "How do I pass NumPy arrays to Fortran without unnecessary copies?", - "how-do-i-pass-numpy-arrays-to-fortran-without-unnecessary-copies", - "../guide/arrays.md", - ), - ("Should I use PRIK or f2py?", "should-i-use-prik-or-f2py", "../performance.md"), - ] - - assert metadata["status"] == "maintained" - assert metadata["publication"] == "reviewed" - previous_start = -1 - for question, anchor_id, target in question_targets: - start_marker = f'
' - start = page.index(start_marker) - end = page.index("
", start) - answer = page[start:end] - assert previous_start < start - assert f"{question}" in answer - assert target in answer - previous_start = start - - assert page.count('
None: - configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - stylesheet = (DOCS_ROOT / "stylesheets/site.css").read_text(encoding="utf-8") - script = (DOCS_ROOT / "javascripts/faq.js").read_text(encoding="utf-8") - - assert "- md_in_html" in configuration - assert "- javascripts/faq.js" in configuration - assert ".prik-faq-item summary" in stylesheet - assert ".prik-faq-item:target" in stylesheet - assert 'target.matches("details.prik-faq-item")' in script - assert "target.open = true" in script - assert 'window.addEventListener("hashchange", openLinkedQuestion)' in script - - -def test_performance_page_bounds_the_prik_f2py_decision() -> None: - page = _visible_documentation_source(DOCS_ROOT / "user/performance.md") - start = page.index("## Should I use PRIK or f2py?") - end = page.index("## Fair, Like-for-Like Setup", start) - comparison = " ".join(page[start:end].split()) - - assert "runtime-call overhead and clean build time" in comparison - assert "They do not rank feature coverage" in comparison - assert "https://numpy.org/doc/stable/f2py/" in comparison - assert "https://numpy.org/doc/stable/f2py/signature-file.html" in comparison - assert "design the Python API, not just generate a wrapper" in comparison - assert "simpler, more Pythonic place to rename or hide exports, flatten modules" in comparison - assert "reorder or hide native arguments, and return native outputs as Python results" in comparison - assert "[NumPy arrays](guide/arrays.md) as complete API contracts" in comparison - assert "dtype, rank, shape, memory layout, contiguity, strides, mutation, and copy behavior" in comparison - assert "[supported positive-stride views](guide/arrays.md#strided-views) without copying" in comparison - assert "[derived types](guide/wrapping-derived-types.md) as Python classes" in comparison - assert "[allocatables](guide/allocatables.md)" in comparison - assert "[pointer forms](guide/pointers.md)" in comparison - assert "native errors as [Python exceptions](guide/error-handling.md)" in comparison - assert "[overloaded procedures](guide/generic-interfaces.md)" in comparison - assert "reference/pyi-contracts/index.md" in comparison - assert "PRIK is currently alpha" in comparison - - -def test_documentation_links_to_documentation_stay_on_the_website() -> None: - github_documentation_prefixes = ( - "https://github.com/PyNumLab/prik/blob/main/docs/", - "https://github.com/PyNumLab/prik/tree/main/docs/", - ) - - for path in DOC_PATHS: - prose_lines: list[str] = [] - fence: str | None = None - for line in _visible_documentation_source(path).splitlines(): - marker = re.match(r"^\s*(`{3,}|~{3,})", line) - if fence is not None: - if line.strip() == fence: - fence = None - continue - if marker is not None: - fence = marker.group(1) - continue - prose_lines.append(line) - - for target in MARKDOWN_LINK.findall("\n".join(prose_lines)): - if "/" not in target and "." not in target: - continue - assert not target.startswith(github_documentation_prefixes), ( - f"{path.relative_to(ROOT)}: documentation link points to GitHub: {target}" - ) - if target.startswith(("http://", "https://", "mailto:")): - continue - resolved = (path.parent / target).resolve() - assert resolved != (ROOT / "README.md").resolve(), ( - f"{path.relative_to(ROOT)}: documentation workflow points to the repository README" - ) - if resolved.is_relative_to(DOCS_ROOT.resolve()): - assert resolved.is_file(), ( - f"{path.relative_to(ROOT)}: documentation link must target a website page or asset: {target}" - ) - - -def test_first_wrapped_function_shows_contract_and_mentions_later_support_boundaries() -> None: - page = (DOCS_ROOT / "user/getting-started/first-wrapped-function.md").read_text(encoding="utf-8") - source_index = page.index("scale.f90") - build_index = page.index("python3 -m prik scale.f90") - command_index = page.index("python3 -m prik generate --pyi scale.f90") - contract_index = page.index( - "@standalone\n@native_call([Addr(Arg(0)), Addr(Arg(1))])\ndef scale(\n" - " value: Float64,\n factor: Float64\n) -> Float64: ..." - ) - docstring_index = page.index("## Inspect the Generated Docstring") - call_index = page.index("## Call the Function") - - assert source_index < command_index < contract_index < build_index - assert build_index < docstring_index < call_index - assert "editable description of the\nPython interface" in page - assert "scale(value, factor) -> float64" in page - assert "assert result == 7.5" in page - assert "isinstance(result, float)" not in page - assert "## Current Limitations" not in page - - -def test_first_wrapped_module_shows_local_input_and_generated_contract() -> None: - page = (DOCS_ROOT / "user/getting-started/first-wrapped-module.md").read_text(encoding="utf-8") - source_index = page.index("module_state.f90") - build_index = page.index("python3 -m prik module_state.f90") - docstring_index = page.index("## Inspect the Generated Docstring") - usage_index = page.index("## Usage Example") - inspect_index = page.index("python3 -m prik generate --pyi module_state.f90") - contract_index = page.index("## Key Rules") - - assert source_index < build_index < docstring_index < usage_index < inspect_index < contract_index - assert "print(mod.__doc__)" in page - assert "module_state\n\nModule Attributes" in page - assert "summarize() -> int32" in page - assert "scaled_counter() -> float64" in page - assert "next_local() -> int32" in page - assert "nmax : int32\n Read-only constant." in page - assert "counter : int32" in page - assert "scale : float64" in page - assert "saved_counter : int32" in page - assert "Assignment writes through to native storage." not in page - assert "fmodule_vars_f90" not in page - assert "## Current Limitations" not in page - - -def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: - page = (DOCS_ROOT / "user/getting-started/beginner-workflow.md").read_text(encoding="utf-8") - source_reference_index = page.index("scale.f90") - layout_index = page.index("src/") - contract_index = page.index("python3 -m prik generate --pyi src/scale.f90") - build_index = page.index("python3 -m prik src/scale.f90") - smoke_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") - editing_index = page.index("## 4. Optionally Edit the Contract") - edited_contract_index = page.index("contracts/scale/__init__.pyi", editing_index) - diagnosis_index = page.index("## 5. Diagnose a Failure") - - assert source_reference_index < layout_index < contract_index < build_index - assert build_index < smoke_index < editing_index < edited_contract_index < diagnosis_index - assert "[First Wrapped Function](first-wrapped-function.md)" in page - assert "scale_api" not in page - - -def test_user_guide_teaches_small_contract_edits_in_context() -> None: - arrays = _visible_documentation_source(DOCS_ROOT / "user/guide/arrays.md") - strings = _visible_documentation_source(DOCS_ROOT / "user/guide/strings.md") - functions = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-functions.md") - modules = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-modules.md") - generics = _visible_documentation_source(DOCS_ROOT / "user/guide/generic-interfaces.md") - derived = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-derived-types.md") - errors = _visible_documentation_source(DOCS_ROOT / "user/guide/error-handling.md") - - assert "Edit the semantic `.pyi` and add `ORDER_C`" in arrays - assert "Edit the declarations in `contracts/strings/strings_api.pyi`" in strings - assert '@bind("scale")' in functions - assert "## Shape the Module API With the Contract" in modules - assert "nmax: Final[Int32] = 12" in modules - assert "counter: Int32 = 9" in modules - assert "scale: Float64 = 2.0" in modules - assert "saved_counter: private[Int32]" in modules - assert "It does not turn a writable Fortran variable into a read-only" in modules - assert "## Flatten Module Namespaces" in modules - assert "from .module1 import *" in modules - assert "from .module2 import *" in modules - assert "library.func1()" in modules - assert "library.module1` and `library.module2` are no longer exported" in modules - assert "the wrapper build fails and asks for an explicit" in modules - assert "## Extend an Overload Set" in generics - assert '@overload("convert_logical")' in generics - assert "## Custom Constructor" in derived - assert '@bind("initialize_point")' in derived - assert "### Expose a Module Procedure as a Method" in derived - assert "def move(self, dx: Float64, dy: Float64)" in derived - assert '@raises(status="status", message="message", success=0)' in errors - - -def test_array_guide_routes_advanced_shape_expressions_to_reference() -> None: - guide = _visible_documentation_source(DOCS_ROOT / "user/guide/arrays.md") - reference = _visible_documentation_source(DOCS_ROOT / "user/reference/pyi-contracts/calls-and-results.md") - - assert "Generated contracts may describe a shape with visible arguments" in guide - assert "../reference/pyi-contracts/calls-and-results.md#advanced-array-shape-expressions" in guide - assert "## Advanced Array Shape Expressions" in reference - assert "Native relationship" in reference - assert "extent_for(n)" in reference - - assert "The bridge emits the signature as an abstract Fortran interface" not in guide - assert "A specification function must be pure" not in guide - assert "compiler-produced `.mod` interfaces" not in guide - - -def test_user_guide_keeps_generated_docstrings_with_new_overload_and_class_features() -> None: - modules = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-modules.md") - generics = _visible_documentation_source(DOCS_ROOT / "user/guide/generic-interfaces.md") - derived = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-derived-types.md") - - assert "## Inspect the Module" not in modules - assert "print(mod.__doc__)" not in modules - - assert "## Inspect the Overloads" in generics - assert "print(conversions.__doc__)" in generics - assert "print(conversions.convert.__doc__)" in generics - assert "convert(value: int32) -> int32" in generics - assert "convert(value: float64) -> float64" in generics - - assert "## Inspect the Class" in derived - assert "print(points.point.__doc__)" in derived - assert "print(points.point.__init__.__doc__)" in derived - assert "`points.point.move.__doc__`" in derived - assert "print(points.point.__add__.__doc__)" in derived - assert "__add__(right: point) -> point" in derived - - -def test_getting_started_pages_keep_advanced_stage_flags_out_of_beginner_path() -> None: - content = "\n".join( - _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_GETTING_STARTED_PAGES - ) - - assert "--parse" not in content - assert "--semantics" not in content - assert "--json" not in content - - -def test_user_guide_shows_direct_shared_library_build() -> None: - content = "\n".join( - _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_USER_GUIDE_PAGES - ) - shared_library = _visible_documentation_source(DOCS_ROOT / "user/guide/building-shared-library.md") - - assert "python3 -m prik src/scale.f90 --out-dir build/scale" in content - assert "[Common Beginner Workflow](../getting-started/beginner-workflow.md)" in shared_library - - -def test_user_compiler_docs_distinguish_runtime_evidence_from_configured_profiles() -> None: - installation = _visible_documentation_source(DOCS_ROOT / "user/getting-started/installation.md") - shared_library = _visible_documentation_source(DOCS_ROOT / "user/guide/building-shared-library.md") - - for compiler_pair in ( - "| `gfortran` | `gcc` | Default; tested on Linux and macOS |", - "| `ifx` | `icx` | Tested on Linux with version 2026.1.1 |", - "| `flang` | `clang` | Tested on Linux and macOS with version 22.1.8 |", - ): - assert compiler_pair in installation - - assert "| `ifort` | `icx` | Recognized; not routinely tested |" in installation - assert "| `nvfortran` | `nvc` | Recognized; not yet tested |" in installation - assert "| `pgfortran` | `pgcc` | Legacy option; not yet tested |" in installation - assert "For the best-tested experience, use GNU, IFX, or Flang." in installation - assert "--compiler ifx" in shared_library - assert "--compiler flang" in shared_library - assert "keeps both compilers in the same family" in shared_library - - -def test_cli_reference_reuses_the_derived_type_points_example() -> None: - content = _visible_documentation_source(CLI_REFERENCE_PATH) - - assert "`points.f90` and `geometry` naming" in content - assert "../guide/wrapping-derived-types.md#complete-example" in content - assert "python3 -m prik parse points.f90" in content - assert "python3 -m prik generate --pyi points.f90 --out contracts" in content - assert "scale.f90" not in content - - -def test_fortran_wrapper_reference_shows_every_common_shared_library_build_input() -> None: - content = _visible_documentation_source(DOCS_ROOT / "user/reference/fortran-wrapper.md") - example = content.split("For example, this command supplies every common build input", maxsplit=1)[1].split( - "`--compiler` selects", maxsplit=1 - )[0] - - for value in ( - "python3 -m prik solver.f90", - "--out solver", - "--out-dir build/solver", - "--compiler gfortran", - "-I include", - "--native-compile-flags=-O3", - "--native-library openblas", - "--verbose", - ): - assert value in example - - -def test_array_handle_docs_keep_views_copies_and_handles_distinct() -> None: - allocatables = _visible_documentation_source(DOCS_ROOT / "user/guide/allocatables.md") - pointers = _visible_documentation_source(DOCS_ROOT / "user/guide/pointers.md") - memory = _visible_documentation_source(DOCS_ROOT / "user/guide/memory-management.md") - - assert "Reading the Python attribute" in allocatables - assert "returns an `Allocatable[T[...]]` handle, not `ndarray | None`." in allocatables - assert "never creates an automatic detached snapshot" in allocatables - assert "A NumPy view reflects current native storage." in allocatables - assert "A pointer-array function result becomes a returned `PointerArray`." in pointers - assert "has persistent descriptor storage, but the target can belong to another" in pointers - assert "`associate(other)` makes two pointer handles refer to the same target" in pointers - assert "If `p2` is unassociated, `p1` becomes" in pointers - assert "Do Not Return A Pointer To Expired Local Storage" in pointers - assert "Derived module variables remain live objects" in memory - assert "Fortran module owns their storage" in memory - - -def test_parameter_array_references_document_read_only_snapshots() -> None: - references = [ - _visible_documentation_source(DOCS_ROOT / "user/reference/fortran-wrapper.md"), - _visible_documentation_source(DOCS_ROOT / "user/reference/generated-modules.md"), - ] - - for reference in references: - normalized_reference = " ".join(reference.split()) - assert "Python-owned" in normalized_reference - assert "read-only NumPy snapshots" in normalized_reference - assert "no native setter" in normalized_reference - - -@pytest.mark.parametrize("heading", CLI_HELP_GROUP_HEADINGS) -def test_cli_help_uses_documented_option_groups(heading: str) -> None: - assert heading in _prik_cli_help() - - -@pytest.mark.parametrize("option", CLI_REFERENCE_OPTIONS) -def test_cli_reference_documents_public_option(option: str) -> None: - content = CLI_REFERENCE_PATH.read_text(encoding="utf-8") - assert option in content - - -@pytest.mark.parametrize("option", CLI_VISIBLE_HELP_OPTIONS) -def test_cli_help_exposes_documented_public_option(option: str) -> None: - assert option in _prik_cli_help() - - -@pytest.mark.parametrize("name", sorted(prik.__all__)) -def test_python_api_reference_documents_public_export(name: str) -> None: - content = PYTHON_API_REFERENCE_PATH.read_text(encoding="utf-8") - assert f"`{name}`" in content - - -@pytest.mark.parametrize("relative_path", REQUIRED_SOURCE_NAVIGATION_PAGES) -def test_required_source_navigation_page_exists(relative_path: str) -> None: - assert (DOCS_ROOT / relative_path).is_file() - - -@pytest.mark.parametrize("relative_path", REQUIRED_SOURCE_NAVIGATION_PAGES) -def test_source_navigation_page_is_in_site_navigation(relative_path: str) -> None: - site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - assert relative_path in site_configuration - - -@pytest.mark.parametrize("relative_path", REQUIRED_EXAMPLE_RECIPE_PAGES) -def test_required_example_recipe_exists(relative_path: str) -> None: - assert (DOCS_ROOT / relative_path).is_file() - - -@pytest.mark.parametrize("relative_path", REQUIRED_EXAMPLE_RECIPE_PAGES) -def test_example_recipe_is_in_site_navigation(relative_path: str) -> None: - site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - assert relative_path in site_configuration - - -@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_HOTSPOTS) -def test_source_navigation_hotspot_exists(relative_path: str) -> None: - assert (ROOT / relative_path).exists() - - -@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_HOTSPOTS) -def test_source_navigation_mentions_hotspot(relative_path: str) -> None: - assert relative_path in _combined_text(SOURCE_NAVIGATION_CORPUS) - - -@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_PUBLIC_DOCS) -def test_source_navigation_public_doc_exists(relative_path: str) -> None: - assert (ROOT / relative_path).is_file() - - -@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_PUBLIC_DOCS) -def test_source_navigation_mentions_public_doc(relative_path: str) -> None: - assert relative_path in _combined_text(SOURCE_NAVIGATION_CORPUS) - - -@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_TEST_TARGETS) -def test_source_navigation_test_target_exists(relative_path: str) -> None: - assert (ROOT / relative_path).exists() - - -@pytest.mark.parametrize("relative_path", SOURCE_NAVIGATION_TEST_TARGETS) -def test_source_navigation_mentions_test_target(relative_path: str) -> None: - assert relative_path in _combined_text(SOURCE_NAVIGATION_CORPUS) - - -@pytest.mark.parametrize("relative_path", PACKAGE_READMES) -def test_package_readme_links_to_source_navigation(relative_path: str) -> None: - content = (ROOT / relative_path).read_text(encoding="utf-8") - for reference in PACKAGE_README_NAVIGATION_REFERENCES: - assert reference in content - - -@pytest.mark.parametrize("relative_path", PACKAGE_READMES) -def test_package_readme_does_not_use_legacy_active_doc_paths(relative_path: str) -> None: - content = (ROOT / relative_path).read_text(encoding="utf-8") - for reference in LEGACY_ACTIVE_DOC_REFERENCES: - assert reference not in content - - -def test_feature_matrix_has_rows_and_status_groups() -> None: - assert FEATURE_MATRIX_ROWS - statuses = {row["Status"] for row in FEATURE_MATRIX_ROWS} - assert {"Supported", "Partially supported", "Unsupported", "Planned", "Not implemented"} <= statuses - - -@pytest.mark.parametrize("feature", FEATURE_MATRIX_REQUIRED_FEATURES) -def test_feature_matrix_includes_required_feature(feature: str) -> None: - matrix_features = {row["Feature"] for row in FEATURE_MATRIX_ROWS} - assert feature in matrix_features - - -@pytest.mark.parametrize("row", FEATURE_MATRIX_ROWS, ids=lambda row: row["Feature"]) -def test_feature_matrix_row_is_complete(row: dict[str, str]) -> None: - assert row["Status"] in FEATURE_MATRIX_STATUSES - assert "TODO" not in " ".join(row.values()) - for column in ["Feature", "Status", "User docs", "Source owner", "Evidence", "Limitations"]: - assert row[column] - for column in ["User docs", "Source owner", "Evidence"]: - assert MARKDOWN_LINK.search(row[column]), f"{row['Feature']}: {column} must contain a Markdown link" - if row["Status"] in {"Supported", "Partially supported"}: - evidence_targets = [ - (FEATURE_MATRIX_PATH.parent / target).resolve() for target in MARKDOWN_LINK.findall(row["Evidence"]) - ] - assert any(target.is_relative_to(ROOT / "tests") for target in evidence_targets), ( - f"{row['Feature']}: support claims need direct test evidence" - ) - - -@pytest.mark.parametrize("row", FEATURE_MATRIX_ROWS, ids=lambda row: row["Feature"]) -def test_feature_matrix_links_point_to_existing_files(row: dict[str, str]) -> None: - for column in ["User docs", "Source owner", "Evidence"]: - for target in MARKDOWN_LINK.findall(row[column]): - if target.startswith(("http://", "https://")): - continue - resolved_target = (FEATURE_MATRIX_PATH.parent / target).resolve() - assert resolved_target.exists(), f"{row['Feature']}: {column} link target does not exist: {target}" - - -@pytest.mark.parametrize("package", MAJOR_SOURCE_PACKAGES) -def test_source_map_covers_major_source_packages(package: str) -> None: - source_map = (DOCS_ROOT / "developer/source-map.md").read_text(encoding="utf-8") - assert package in source_map - - -@pytest.mark.parametrize("relative_path", PACKAGE_READMES) -def test_major_source_package_has_local_readme(relative_path: str) -> None: - assert (ROOT / relative_path).is_file() - - -@pytest.mark.parametrize("relative_path", ARCHIVED_OLD_DOCS) -def test_old_documentation_is_archived(relative_path: str) -> None: - assert (DOCS_ROOT / relative_path).is_file() - - -@pytest.mark.parametrize("relative_path", OLD_TOP_LEVEL_DOCS) -def test_old_top_level_documentation_was_moved(relative_path: str) -> None: - assert not (DOCS_ROOT / relative_path).exists() - - -def test_static_site_seed_configuration_exists() -> None: - assert (ROOT / "mkdocs.yml").is_file() - - -def test_site_theme_keeps_sidebar_open_and_code_blocks_copyable() -> None: - site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - assert "name: readthedocs" in site_configuration - assert "collapse_navigation: false" in site_configuration - assert "navigation_depth: 4" in site_configuration - assert "stylesheets/site.css" in site_configuration - assert "stylesheets/code-copy.css" in site_configuration - assert "javascripts/code-copy.js" in site_configuration - - script = (DOCS_ROOT / "javascripts" / "code-copy.js").read_text(encoding="utf-8") - layout_stylesheet = (DOCS_ROOT / "stylesheets" / "site.css").read_text(encoding="utf-8") - stylesheet = (DOCS_ROOT / "stylesheets" / "code-copy.css").read_text(encoding="utf-8") - assert ".wy-nav-content" in layout_stylesheet - assert "max-width: 1200px" in layout_stylesheet - assert "margin: 0" in layout_stylesheet - assert ".wy-nav-side" in layout_stylesheet - assert "padding-bottom: 0" in layout_stylesheet - assert ".wy-side-scroll" in layout_stylesheet - assert "overflow-y: auto" in layout_stylesheet - assert "scrollbar-width: thin" in layout_stylesheet - assert ".wy-side-scroll::-webkit-scrollbar-thumb" in layout_stylesheet - assert ".rst-versions" in layout_stylesheet - assert "display: none" in layout_stylesheet - assert ".rst-content pre" in layout_stylesheet - assert "width: 100%" in layout_stylesheet - assert "max-width: 56rem" in layout_stylesheet - assert "padding-right: 3.25rem" in layout_stylesheet - assert 'document.querySelectorAll("pre code")' in script - assert "navigator.clipboard.writeText" in script - assert 'button.setAttribute("aria-label", "Copy code to clipboard")' in script - assert ".prik-code-copy" in stylesheet diff --git a/tests/tools/README.md b/tests/tools/README.md new file mode 100644 index 000000000..037ab7269 --- /dev/null +++ b/tests/tools/README.md @@ -0,0 +1,11 @@ +# Maintainer Tool Tests + +`tests/tools/` owns behavior tests for repository-maintained commands and CI +support scripts below `tools/`. These tests exercise arguments, output, error +handling, and exit status; they do not freeze test or workflow organization. + +Run this owner with: + +```bash +python3 -m pytest -q tests/tools +``` diff --git a/tests/shared/tools/test_benchmark_host.py b/tests/tools/test_benchmark_host.py similarity index 100% rename from tests/shared/tools/test_benchmark_host.py rename to tests/tools/test_benchmark_host.py diff --git a/tests/shared/tools/test_build_time_benchmark.py b/tests/tools/test_build_time_benchmark.py similarity index 100% rename from tests/shared/tools/test_build_time_benchmark.py rename to tests/tools/test_build_time_benchmark.py diff --git a/tests/shared/tools/test_check_benchmark_regression.py b/tests/tools/test_check_benchmark_regression.py similarity index 100% rename from tests/shared/tools/test_check_benchmark_regression.py rename to tests/tools/test_check_benchmark_regression.py diff --git a/tests/shared/tools/test_check_radon_policy.py b/tests/tools/test_check_radon_policy.py similarity index 100% rename from tests/shared/tools/test_check_radon_policy.py rename to tests/tools/test_check_radon_policy.py diff --git a/tests/shared/tools/test_check_static_analysis_versions.py b/tests/tools/test_check_static_analysis_versions.py similarity index 100% rename from tests/shared/tools/test_check_static_analysis_versions.py rename to tests/tools/test_check_static_analysis_versions.py diff --git a/tests/shared/tools/test_generate_performance_docs.py b/tests/tools/test_generate_performance_docs.py similarity index 100% rename from tests/shared/tools/test_generate_performance_docs.py rename to tests/tools/test_generate_performance_docs.py diff --git a/tests/shared/tools/test_print_pytest_failures.py b/tests/tools/test_print_pytest_failures.py similarity index 100% rename from tests/shared/tools/test_print_pytest_failures.py rename to tests/tools/test_print_pytest_failures.py diff --git a/tests/shared/tools/test_run_fortran_toolchain_lane.py b/tests/tools/test_run_fortran_toolchain_lane.py similarity index 100% rename from tests/shared/tools/test_run_fortran_toolchain_lane.py rename to tests/tools/test_run_fortran_toolchain_lane.py diff --git a/tests/shared/tools/test_runtime_benchmark.py b/tests/tools/test_runtime_benchmark.py similarity index 100% rename from tests/shared/tools/test_runtime_benchmark.py rename to tests/tools/test_runtime_benchmark.py diff --git a/tests/shared/tools/test_warm_real_library_native_cache.py b/tests/tools/test_warm_real_library_native_cache.py similarity index 100% rename from tests/shared/tools/test_warm_real_library_native_cache.py rename to tests/tools/test_warm_real_library_native_cache.py diff --git a/tests/workflows/README.md b/tests/workflows/README.md new file mode 100644 index 000000000..534dfea37 --- /dev/null +++ b/tests/workflows/README.md @@ -0,0 +1,13 @@ +# Workflow Tests + +`tests/workflows/` owns exceptional safety checks for automation under +`.github/workflows/`. Add a workflow test only when it protects a concrete, +costly risk that the workflow service itself will not reliably expose before +damage occurs. Do not use this directory to freeze job names, test paths, or +the current CI organization. + +Run this owner with: + +```bash +python3 -m pytest -q tests/workflows +``` diff --git a/tests/workflows/test_release.py b/tests/workflows/test_release.py new file mode 100644 index 000000000..662daaa3a --- /dev/null +++ b/tests/workflows/test_release.py @@ -0,0 +1,20 @@ +"""Release workflow safety contracts.""" + +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).parents[2] +PUBLISH_WORKFLOW = REPOSITORY_ROOT / ".github/workflows/publish-to-pypi.yml" + + +def test_pypi_publication_uses_protected_token_free_release_job() -> None: + workflow = PUBLISH_WORKFLOW.read_text(encoding="utf-8") + build_job, publish_job = workflow.split(" publish:\n", maxsplit=1) + + assert "id-token: write" not in build_job + assert "needs: build" in publish_job + assert "name: pypi" in publish_job + assert "id-token: write" in publish_job + assert "uses: pypa/gh-action-pypi-publish@release/v1" in publish_job + assert "secrets." not in workflow + assert "password:" not in workflow From 05e03a234646adf0009ca06de058780897c9f9db Mon Sep 17 00:00:00 2001 From: said Date: Mon, 10 Aug 2026 16:20:45 +0100 Subject: [PATCH 2/3] add githooks --- .githooks/pre-push | 28 ++++++++++++++++++++++++++ CHANGELOG.md | 4 +++- docs/developer/development-workflow.md | 12 +++++++++++ docs/developer/quality-assurance.md | 1 + docs/developer/testing-strategy.md | 5 ++++- tests/README.md | 4 ++++ 6 files changed, 52 insertions(+), 2 deletions(-) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 000000000..fde30c627 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Run repository-maintainer checks before Git sends a push.""" + +from pathlib import Path +import subprocess +import sys + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +REQUIRED_TESTS = ("tests/tools", "tests/workflows") + + +def main() -> int: + """Return pytest's status for the required local pre-push suites.""" + print( + "Running required pre-push tests: tests/tools tests/workflows", + flush=True, + ) + completed = subprocess.run( # noqa: S603 + [sys.executable, "-m", "pytest", "-q", *REQUIRED_TESTS], + cwd=REPOSITORY_ROOT, + check=False, + ) + return completed.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ebddddc..b578004c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,9 @@ release tags add a leading `v` to the package version. `tests/tools/`, removed the generic `tests/shared/` bucket, and mirrored internal tests by production package with narrower support helpers; removed recursive layout-policing tests that froze maintainer organization, retaining - exceptional release safety under `tests/workflows/`. + exceptional release safety under `tests/workflows/`. The maintainer-tool and + workflow-safety suites now also run through the repository's tracked pre-push + hook for earlier local feedback while remaining enforced by GitHub Actions. - Simplified the documented DGESV validation and the LAPACK test suite to use explicit NumPy Fortran-order copies, with documented numerical-test helper conventions. diff --git a/docs/developer/development-workflow.md b/docs/developer/development-workflow.md index 7c2b56b34..9f23c3fb3 100644 --- a/docs/developer/development-workflow.md +++ b/docs/developer/development-workflow.md @@ -1337,6 +1337,18 @@ PYTHONPATH=. pytest -q tests/tools PYTHONPATH=. pytest -q tests/workflows ``` +Maintainer-tool and workflow-safety tests run locally before every push. Enable +the tracked hook once in each clone: + +```bash +git config core.hooksPath .githooks +``` + +The hook runs `tests/tools/` and `tests/workflows/` and rejects the push if +either suite fails. GitHub Actions runs them again as the shared enforcement +boundary alongside the required product, documentation, compiler, coverage, +and real-library checks. + As a project policy, do not merge pull requests unless all checks are green. ### Fixture Maintenance diff --git a/docs/developer/quality-assurance.md b/docs/developer/quality-assurance.md index e083347a4..d44ee33d2 100644 --- a/docs/developer/quality-assurance.md +++ b/docs/developer/quality-assurance.md @@ -23,6 +23,7 @@ testing and pre-commit are not part of the active stack. | Cadence | Tools | | --- | --- | +| Local pre-push | Maintainer-tool tests in `tests/tools/` and workflow-safety tests in `tests/workflows/` | | Pull request and protected-branch push | pytest, bounded property/fuzz cases, stable-seed pytest-randomly, Ruff, Bandit, Vulture, staged Radon policy, and project coverage | | Pull request, protected-branch push, weekly, and manual | Pinned Intel IFX/ICX and LLVM Flang/Clang profile checks plus strict Fortran toolchain smoke | | Validated pull request and main-branch push | Pinned ARM64 prik/f2py correctness and rigorous performance benchmark | diff --git a/docs/developer/testing-strategy.md b/docs/developer/testing-strategy.md index a53934a60..db859c851 100644 --- a/docs/developer/testing-strategy.md +++ b/docs/developer/testing-strategy.md @@ -202,7 +202,10 @@ validators stay parameterized over applicable pages. Page-specific content contracts live in a module named for the documentation area they protect. Workflow checks belong under `tests/workflows/` only when they protect concrete behavior or release safety; they should not duplicate or -freeze the current CI organization. +freeze the current CI organization. Tests for maintainer tools and workflow +safety run through the tracked pre-push hook for early feedback and remain in +GitHub Actions for shared enforcement. Enable the hook once per clone with +`git config core.hooksPath .githooks`. Test support contains reusable construction or assertion behavior, not an alternate import surface for production code. Tests import `pytest`, Python diff --git a/tests/README.md b/tests/README.md index 95957536d..5673cc05f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,6 +24,10 @@ language. | [`tests/tools/`](tools/README.md) | Maintainer commands and CI support scripts | `python3 -m pytest -q tests/tools` | | [`tests/workflows/`](workflows/README.md) | Exceptional safety properties for repository automation | `python3 -m pytest -q tests/workflows` | +The tracked pre-push hook runs `tests/tools/` and `tests/workflows/` locally; +activate it once per clone with `git config core.hooksPath .githooks`. GitHub +Actions runs both suites again as the shared enforcement boundary. + The Fortran feature index maps each maintained User Guide and semantic `.pyi` page to its final directory and focused command. The cleanup contract and progress gates live in From b6b2d21955d24acab91868b408049faa37244cfe Mon Sep 17 00:00:00 2001 From: said Date: Mon, 10 Aug 2026 16:40:19 +0100 Subject: [PATCH 3/3] update githooks --- .githooks/pre-push | 118 ++++++++++++++++++++++--- CHANGELOG.md | 6 +- docs/developer/development-workflow.md | 18 ++-- docs/developer/quality-assurance.md | 2 +- docs/developer/testing-strategy.md | 7 +- tests/README.md | 8 +- 6 files changed, 132 insertions(+), 27 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index fde30c627..6763c652b 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,27 +1,119 @@ #!/usr/bin/env python3 -"""Run repository-maintainer checks before Git sends a push.""" +"""Run quick blocking checks before Git sends a push.""" -from pathlib import Path import subprocess import sys +from pathlib import Path REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +DOCUMENTATION_SMOKE_TESTS = ( + "tests/docs/test_publication.py", + "tests/docs/test_user_content.py", +) +WRAPPER_SMOKE_TEST = ( + "tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::" + "test_fortran_wrapper_default_module_name_does_not_collide_with_root_function" +) REQUIRED_TESTS = ("tests/tools", "tests/workflows") -def main() -> int: - """Return pytest's status for the required local pre-push suites.""" - print( - "Running required pre-push tests: tests/tools tests/workflows", - flush=True, - ) - completed = subprocess.run( # noqa: S603 - [sys.executable, "-m", "pytest", "-q", *REQUIRED_TESTS], - cwd=REPOSITORY_ROOT, - check=False, +def _available_main_ref(remote_name: str) -> str: + """Return the best local ref for the remote's pre-push main branch.""" + candidates = (f"{remote_name}/main", "origin/main", "main") + for candidate in dict.fromkeys(candidates): + result = subprocess.run( # noqa: S603 + ["git", "rev-parse", "--verify", "--quiet", candidate], + cwd=REPOSITORY_ROOT, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode == 0: + return candidate + return "main" + + +def _required_commands(base_ref: str) -> tuple[tuple[str, tuple[str, ...]], ...]: + """Return the ordered, fail-fast pre-push command set.""" + return ( + ( + "Static-analysis tool versions", + (sys.executable, "tools/check_static_analysis_versions.py"), + ), + ("Ruff lint", (sys.executable, "-m", "ruff", "check", ".")), + ( + "Ruff format", + (sys.executable, "-m", "ruff", "format", "--check", "."), + ), + ( + "Codegen complexity", + (sys.executable, "tools/check_codegen_complexity.py"), + ), + ( + "Bandit", + ( + sys.executable, + "-m", + "bandit", + "-q", + "-c", + "pyproject.toml", + "-r", + "prik", + "--severity-level", + "medium", + "--confidence-level", + "medium", + ), + ), + ("Vulture", (sys.executable, "-m", "vulture")), + ( + f"Radon policy against {base_ref}", + ( + sys.executable, + "tools/check_radon_policy.py", + "--base-ref", + base_ref, + ), + ), + ( + "Documentation smoke", + ( + sys.executable, + "-m", + "pytest", + "-q", + *DOCUMENTATION_SMOKE_TESTS, + ), + ), + ( + "Compiled scalar-wrapper smoke", + (sys.executable, "-m", "pytest", "-q", WRAPPER_SMOKE_TEST), + ), + ( + "Maintainer and workflow tests", + (sys.executable, "-m", "pytest", "-q", *REQUIRED_TESTS), + ), ) - return completed.returncode + + +def main() -> int: + """Run each required check and return the first failing status.""" + remote_name = sys.argv[1] if len(sys.argv) > 1 else "origin" + base_ref = _available_main_ref(remote_name) + for label, command in _required_commands(base_ref): + print(f"\n==> {label}", flush=True) + completed = subprocess.run( # noqa: S603 + command, + cwd=REPOSITORY_ROOT, + check=False, + ) + if completed.returncode != 0: + print(f"Pre-push checks stopped at: {label}", file=sys.stderr) + return completed.returncode + print("\nAll pre-push checks passed.", flush=True) + return 0 if __name__ == "__main__": diff --git a/CHANGELOG.md b/CHANGELOG.md index b578004c1..07cf24c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,10 @@ release tags add a leading `v` to the package version. internal tests by production package with narrower support helpers; removed recursive layout-policing tests that froze maintainer organization, retaining exceptional release safety under `tests/workflows/`. The maintainer-tool and - workflow-safety suites now also run through the repository's tracked pre-push - hook for earlier local feedback while remaining enforced by GitHub Actions. + workflow-safety suites, blocking static analysis, and focused documentation + smoke checks now also run through the repository's tracked pre-push hook, + together with one compiled scalar-wrapper smoke test, for earlier local + feedback while remaining enforced by GitHub Actions. - Simplified the documented DGESV validation and the LAPACK test suite to use explicit NumPy Fortran-order copies, with documented numerical-test helper conventions. diff --git a/docs/developer/development-workflow.md b/docs/developer/development-workflow.md index 9f23c3fb3..e7c31b72a 100644 --- a/docs/developer/development-workflow.md +++ b/docs/developer/development-workflow.md @@ -1337,17 +1337,23 @@ PYTHONPATH=. pytest -q tests/tools PYTHONPATH=. pytest -q tests/workflows ``` -Maintainer-tool and workflow-safety tests run locally before every push. Enable -the tracked hook once in each clone: +Maintainer-tool tests, workflow-safety tests, focused documentation smoke, one +compiled scalar-wrapper smoke test, and blocking static analysis run locally +before every push. Enable the tracked hook once in each clone: ```bash git config core.hooksPath .githooks ``` -The hook runs `tests/tools/` and `tests/workflows/` and rejects the push if -either suite fails. GitHub Actions runs them again as the shared enforcement -boundary alongside the required product, documentation, compiler, coverage, -and real-library checks. +The hook runs static-analysis version validation, Ruff lint and formatting, +the codegen-complexity policy, Bandit, Vulture, the changed-code Radon policy, +the publication and user-content documentation smoke tests, one small public +CLI-to-native-call wrapper test, `tests/tools/`, and `tests/workflows/`. It +rejects the push on the first failure. GitHub Actions runs these checks again +as the shared enforcement boundary alongside the required product, complete +documentation, compiler, coverage, and real-library checks. The slower +documentation validators, verbose advisory Radon reports, and broader compiled +smoke matrix remain outside the quick local hook. As a project policy, do not merge pull requests unless all checks are green. diff --git a/docs/developer/quality-assurance.md b/docs/developer/quality-assurance.md index d44ee33d2..67f907243 100644 --- a/docs/developer/quality-assurance.md +++ b/docs/developer/quality-assurance.md @@ -23,7 +23,7 @@ testing and pre-commit are not part of the active stack. | Cadence | Tools | | --- | --- | -| Local pre-push | Maintainer-tool tests in `tests/tools/` and workflow-safety tests in `tests/workflows/` | +| Local pre-push | Blocking static analysis, focused documentation smoke, one compiled scalar-wrapper smoke test, maintainer-tool tests in `tests/tools/`, and workflow-safety tests in `tests/workflows/` | | Pull request and protected-branch push | pytest, bounded property/fuzz cases, stable-seed pytest-randomly, Ruff, Bandit, Vulture, staged Radon policy, and project coverage | | Pull request, protected-branch push, weekly, and manual | Pinned Intel IFX/ICX and LLVM Flang/Clang profile checks plus strict Fortran toolchain smoke | | Validated pull request and main-branch push | Pinned ARM64 prik/f2py correctness and rigorous performance benchmark | diff --git a/docs/developer/testing-strategy.md b/docs/developer/testing-strategy.md index db859c851..b9df372b5 100644 --- a/docs/developer/testing-strategy.md +++ b/docs/developer/testing-strategy.md @@ -203,8 +203,11 @@ contracts live in a module named for the documentation area they protect. Workflow checks belong under `tests/workflows/` only when they protect concrete behavior or release safety; they should not duplicate or freeze the current CI organization. Tests for maintainer tools and workflow -safety run through the tracked pre-push hook for early feedback and remain in -GitHub Actions for shared enforcement. Enable the hook once per clone with +safety and all blocking static-analysis checks run through the tracked pre-push +hook for early feedback, together with the fast publication and user-content +documentation checks and one compiled scalar-wrapper test that exercises the +public source-build path through a native call. They remain in GitHub Actions +for shared enforcement. Enable the hook once per clone with `git config core.hooksPath .githooks`. Test support contains reusable construction or assertion behavior, not an diff --git a/tests/README.md b/tests/README.md index 5673cc05f..e9f036fc8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,9 +24,11 @@ language. | [`tests/tools/`](tools/README.md) | Maintainer commands and CI support scripts | `python3 -m pytest -q tests/tools` | | [`tests/workflows/`](workflows/README.md) | Exceptional safety properties for repository automation | `python3 -m pytest -q tests/workflows` | -The tracked pre-push hook runs `tests/tools/` and `tests/workflows/` locally; -activate it once per clone with `git config core.hooksPath .githooks`. GitHub -Actions runs both suites again as the shared enforcement boundary. +The tracked pre-push hook runs the blocking static-analysis gate, the focused +publication and user-content documentation smoke tests, one compiled scalar +source-to-native-call wrapper test, `tests/tools/`, and `tests/workflows/` +locally; activate it once per clone with `git config core.hooksPath .githooks`. +GitHub Actions runs the checks again as the shared enforcement boundary. The Fortran feature index maps each maintained User Guide and semantic `.pyi` page to its final directory and focused command. The cleanup contract and