diff --git a/.github/actions/CI-5974-Fetch-CPython/action.yaml b/.github/actions/CI-5974-Fetch-CPython/action.yaml new file mode 100644 index 00000000000..abc04c5ee50 --- /dev/null +++ b/.github/actions/CI-5974-Fetch-CPython/action.yaml @@ -0,0 +1,202 @@ +--- +name: 'Checkout CPython' +description: 'checks-out the given CPython version' +author: 'Mr. Walls' +branding: + icon: 'download-cloud' + color: 'blue' +inputs: + override-repository: + description: | + The GitHub repository to clone CPython from. When running this action on github.com, + the default value is sufficient. Useful for Forks. + required: true + default: 'python/cpython' + override-rustpython-path: + description: | + override value for path to the Python Lib. The default is to use the value of the environment + variable 'RUSTPYTHONPATH'. Most users will find the default 'Lib' sufficient. + required: true + default: '' + override-path: + description: | + Path to setup. When running this action on github.com, the default value + is sufficient. MUST be a path to a directory named 'cpython'. + required: true + default: ${{ github.server_url == 'https://github.com' && github.workspace || 'cpython' }} + override-cpython-lib-path: + description: | + override value for path to the CPython Reference Lib. The default is to use the value of the + environment variable 'PYTHONPLATLIBDIR'. Most users will find the default 'Lib' sufficient. + See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPLATLIBDIR for more. + required: true + default: '' + match: + description: | + Glob-style pattern of files or directories to match and integrate. + Only works with git tracked files. + required: true + type: string + default: 'Lib/test/*.py Lib/test/**/*.py' + ignore: + description: | + List of Glob-style patterns of files or directories to ignore. + Only works with git tracked files. + required: false + type: string + github-token: + description: | + The token used to authenticate when fetching RustPython commits from + https://github.com/RustPython/RustPython.git. When running this action on github.com, + the default value is sufficient. When running on GHES, you can pass a personal access + token for github.com if you are experiencing rate limiting. + default: ${{ github.server_url == 'https://github.com' && github.token || '' }} + required: true + python-version: + description: | + The Cpython version (e.g., any valid release or tag, 3.12, 3.13, 3.14) to override setup. + The default is to use the value of the environment variable 'PYTHON_VERSION'. + default: '3.13' + required: true +outputs: + branch-name: + description: "The name of the branch that was checked-out." + value: ${{ steps.output_branch_name.outputs.branch-name || '' }} + sha: + description: "The SHA of the commit checked-out." + value: ${{ steps.output_sha.outputs.sha || 'HEAD' }} + files: + description: "The downloaded artifact-files." + value: ${{ steps.output_cpython_files.outputs.files }} + +runs: + using: composite + steps: + - name: "Setup Python" + id: output_python + env: + PYTHON_VERSION_INPUT: ${{ inputs.python-version }} + shell: bash + run: | + if [[ -n $PYTHON_VERSION_INPUT ]]; then + printf "python-version=%s\n" "${PYTHON_VERSION_INPUT}" >> "$GITHUB_OUTPUT" + PYTHON_VERSION=${PYTHON_VERSION_INPUT} + else + printf "python-version=%s\n" "${PYTHON_VERSION}" >> "$GITHUB_OUTPUT" + fi + printf "%s\n" "PYTHON_VERSION=${PYTHON_VERSION}" >> "$GITHUB_ENV" + - name: "Setup RustPython Lib Path" + id: output_rpython_path + env: + RUST_PYTHON_LIB_PATH_INPUT: ${{ inputs.override-rustpython-path }} + shell: bash + run: | + if [[ -n $RUST_PYTHON_LIB_PATH_INPUT ]]; then + printf "::debug:: Initializing rust-python-path as ${RUST_PYTHON_LIB_PATH_INPUT}" + printf "rust-python-path=%s\n" "${RUST_PYTHON_LIB_PATH_INPUT:-${RUSTPYTHONPATH:-Lib}}" >> "$GITHUB_OUTPUT" + else + printf "::debug:: Initializing rust-python-path as ${RUSTPYTHONPATH:-Lib}" + printf "rust-python-path=%s\n" "${RUSTPYTHONPATH:-Lib}" >> "$GITHUB_OUTPUT" + fi + printf "%s\n" "RUSTPYTHONPATH=${RUST_PYTHON_LIB_PATH_INPUT:-${RUSTPYTHONPATH:-Lib}}" >> "$GITHUB_ENV" + - name: "Setup cPython Lib Path" + id: output_cpython_path + env: + PYTHONPLATLIBDIR_INPUT: ${{ inputs.override-cpython-lib-path }} + shell: bash + run: | + if [[ -n $PYTHONPLATLIBDIR_INPUT ]]; then + printf "::debug:: Initializing cpython-lib-path as ${PYTHONPLATLIBDIR_INPUT}" + printf "cpython-lib-path=%s\n" "${PYTHONPLATLIBDIR_INPUT:-${PYTHONPLATLIBDIR:-Lib}}" >> "$GITHUB_OUTPUT" + else + printf "::debug:: Initializing cpython-lib-path as ${PYTHONPLATLIBDIR:-Lib}" + printf "cpython-lib-path=%s\n" "${PYTHONPLATLIBDIR:-Lib}" >> "$GITHUB_OUTPUT" + fi + printf "%s\n" "PYTHONPLATLIBDIR=${PYTHONPLATLIBDIR_INPUT:-${PYTHONPLATLIBDIR:-Lib}}" >> "$GITHUB_ENV" + - name: Fetch Reference Cpython ${{ matrix.python-version }} on ${{ matrix.os }} + id: cpython + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: ${{ inputs.override-path }} + fetch-tags: true + sparse-checkout: | + ${{ steps.output_cpython_path.outputs.cpython-lib-path }} + ref: ${{ steps.output_python.outputs.python-version }} + repository: ${{ inputs.override-repository }} + # fixed settings + fetch-depth: 0 + sparse-checkout-cone-mode: false + submodules: true + token: ${{ inputs.github-token }} + - id: store_old_path + if: ${{ !cancelled() }} + shell: bash + run: | + cd ${PWD:-.} ; + export OLD_PWD=$(pwd) ; # only local use for bootstrap + printf "initial-path=%s\n" "${OLD_PWD}" >> "$GITHUB_OUTPUT" + - id: output_branch_name + if: ${{ !cancelled() }} + shell: bash + run: | + cd ${{ inputs.override-path }} || exit 14 ; + printf "branch-name=%s\n" $(git name-rev --name-only HEAD | cut -d~ -f1-1) >> "$GITHUB_OUTPUT" + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + - id: output_sha + shell: bash + run: | + cd ${{ inputs.override-path }} || exit 14 ; + printf "sha=%s\n" $(git log -1 --format=%H) >> "$GITHUB_OUTPUT" + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + - name: Configure Ignored Reference Lib Files + id: refignorefiles + shell: bash + env: + GIT_IGNORE_PATTERN: ${{ inputs.ignore || '' }} + run: | + # TODO: include work from RustPython/scripts/notes.txt + cd ${{ inputs.override-path }} || exit 14 ; + if [[ -w ".git/info/exclude" ]] ; then + printf "%s\n" ${GIT_IGNORE_PATTERN:-} >>".git/info/exclude" || : ; + else + printf "::debug::%s\n" "Could not find .git/info/exclude" ; + printf "%s\n" ${GIT_IGNORE_PATTERN:-} >>".gitignore" || : ; + fi ; + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + if: ${{ success() }} + - name: Enumerate Reference Lib Files + id: output_cpython_files + shell: bash + env: + TEST_MATCH_PATTERN: ${{ inputs.match || '' }} + run: | + cd ${{ inputs.override-path }} || exit 14 ; + FILES=$(git ls-files --exclude-standard -- ${{ env.TEST_MATCH_PATTERN }} ) + if [ -z "$FILES" ]; then + printf "%s\n" "::warning file=.github/actions/:: No ${{ steps.output_cpython_path.outputs.cpython-lib-path }} Reference files found for Cpython ${{ inputs.python-version }} on ${{ runner.os }}." ; + printf "%s\n" "files=" >> "$GITHUB_OUTPUT" + else + printf "%s\n" "Reference files found:" + printf "%s\n" "$FILES" + # Replace line breaks with commas for GitHub Action Output + FILES="${FILES//$'\n'/ }" + printf "%s\n" "files=$FILES" >> "$GITHUB_OUTPUT" + fi + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + if: ${{ success() }} + - name: "License" + id: show_cpython_license + shell: bash + if: ${{ !cancelled() }} + run: | + cd ${{ inputs.override-path }} || exit 14 ; + if [[ -r LICENSE ]] ; then + printf "\n\n" + cat > "$GITHUB_OUTPUT" + else + printf "::debug:: Initializing rust-python-path as ${RUSTPYTHONPATH:-Lib}" + printf "rust-python-path=%s\n" "${RUSTPYTHONPATH:-Lib}" >> "$GITHUB_OUTPUT" + fi + printf "%s\n" "RUSTPYTHONPATH=${RUST_PYTHON_LIB_PATH_INPUT:-${RUSTPYTHONPATH:-Lib}}" >> "$GITHUB_ENV" + - name: Checkout repository + id: rpython + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: ${{ inputs.override-path }} + persist-credentials: false + ref: ${{ inputs.override-ref }} + repository: ${{ inputs.override-repository }} + # fixed settings + fetch-depth: 0 + sparse-checkout-cone-mode: false + submodules: true + token: ${{ inputs.github-token }} + - id: store_old_path + if: ${{ !cancelled() }} + shell: bash + run: | + cd ${PWD:-.} ; + export OLD_PWD=$(pwd) ; # only local use for bootstrap + printf "initial-path=%s\n" "${OLD_PWD}" >> "$GITHUB_OUTPUT" ; + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then + printf "::debug::Return directory now set to '%s'\n" "${OLD_PWD}" ; + printf "::debug::Working directory is set to '%s'\n" '${{ inputs.override-path }}' ; + fi ; + - id: output_branch_name + if: ${{ !cancelled() }} + shell: bash + run: | + cd ${{ inputs.override-path }} || exit 13 ; + printf "branch-name=%s\n" $(git name-rev --name-only HEAD | cut -d~ -f1-1) >> "$GITHUB_OUTPUT" + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + - id: output_sha + shell: bash + run: | + cd ${{ inputs.override-path }} || exit 13 ; + printf "sha=%s\n" $(git log -1 --format=%H) >> "$GITHUB_OUTPUT" + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + - name: "Setup Cargo" + id: output_cargo_args + shell: bash + run: | + if [[ -n $CARGO_ARGS ]]; then + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then printf "::debug::CARGO_ARGS already set to '%s'\n" "${CARGO_ARGS}" ; fi ; + # e.g., CARGO_ARGS=${CARGO_ARGS} + else + CARGO_ARGS="--release" ; + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then printf "::debug::CARGO_ARGS initialized to '%s'\n" "${CARGO_ARGS}" ; fi ; + fi ; + printf "%s\n" "CARGO_ARGS=${CARGO_ARGS}" >> "$GITHUB_ENV" ; + - name: Pre-Test Build check + id: build_rpython + shell: bash + env: + OS: ${{ runner.os }} + if: ${{ !cancelled() }} + run: | + cd ${{ inputs.override-path }} || exit 13 ; + printf "::group::%s\n" "Cargo" ; + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then printf "::debug::Now Building '%s'\n" "RustPython" ; fi ; + # Execute the testing command in a subshell + ( + RUSTPYTHONPATH=${{ steps.setup_rpython_path.outputs.rust-python-path }} cargo run $CARGO_ARGS -- --version || printf "::error title='build failure':: Could not pass build step for version check on ${OS}.\n" ; + ) ; + printf "::endgroup::%s\n" ; + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + - id: output_rpython_path + shell: bash + run: | + cd ${{ inputs.override-path }} || exit 13 ; # in case it is relative + cd ${{ steps.setup_rpython_path.outputs.rust-python-path }} || exit 13 ; + printf "RUSTPYTHONPATH=%s\n" $(pwd) >> "$GITHUB_ENV" ; + printf "rustpython-lib-path=%s\n" $(pwd) >> "$GITHUB_OUTPUT" ; + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; diff --git a/.github/actions/CI-5974-Integrate-CPython/action.yaml b/.github/actions/CI-5974-Integrate-CPython/action.yaml new file mode 100644 index 00000000000..16918a3226d --- /dev/null +++ b/.github/actions/CI-5974-Integrate-CPython/action.yaml @@ -0,0 +1,82 @@ +--- +name: 'Integrate Reference Implementation' +description: 'Copy Reference Implementation' +author: 'Mr. Walls' +branding: + icon: 'copy' + color: 'purple' +inputs: + into-path: + description: | + Path to Destination. Default is 'rustpython' + required: true + default: 'rustpython' + from-path: + description: | + Path to source. Default is 'cpython' + required: true + default: 'cpython' + files: + description: | + List of paths to copy from source to destination. Default is 'Lib/**/*.py' + required: true + default: 'Lib/**/*.py' + python-version: + description: | + The Cpython version (e.g., any valid release or tag, 3.11, 3.12, 3.13) to override setup. + The default is to use the value of the environment variable 'PYTHON_VERSION'. + default: '3.13' + required: true + +# TODO: add verification steps + +runs: + using: composite + steps: + - id: output_python + env: + PYTHON_VERSION_INPUT: ${{ inputs.python-version }} + name: "Detect Python" + if: ${{ inputs.files != '' }} + shell: bash + run: | + # check the python version + if [[ -n $PYTHON_VERSION_INPUT ]]; then + printf "python-version=%s\n" "${PYTHON_VERSION_INPUT}" >> "$GITHUB_OUTPUT" + PYTHON_VERSION=${PYTHON_VERSION_INPUT} + else + printf "python-version=%s\n" "${PYTHON_VERSION}" >> "$GITHUB_OUTPUT" + fi + printf "%s\n" "PYTHON_VERSION=${PYTHON_VERSION}" >> "$GITHUB_ENV" + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then printf "::debug::Targeting Cpython %s.\n" "${PYTHON_VERSION}" ; fi ; + - id: setup_mkdir_option + if: ${{ !cancelled() }} + shell: bash + run: | + # set mkdir options per platform + if [[ ${{ runner.os }} != 'Windows' ]] ; then + printf "mkdir-args=%s\n" "-v -p -m 751" >> "$GITHUB_OUTPUT" + printf "MKDIR_ARGS=%s\n" "-v -p -m 751" >> "$GITHUB_ENV" + else + printf "mkdir-args=%s\n" "-v -p" >> "$GITHUB_OUTPUT" + printf "MKDIR_ARGS=%s\n" "-v -p" >> "$GITHUB_ENV" + fi + - name: "Integrate Cpython Test files" + id: merge_theirs + shell: bash + if: ${{ !cancelled() && inputs.files != '' }} + env: + INPUT_FILES: ${{ inputs.files }} + run: | + printf "::group::%s\n" "Copy Reference Implementation" ; + for reference_file in ${INPUT_FILES}; do + [[ -d $(dirname ${{ inputs.into-path }}/"${reference_file}" ) ]] || mkdir $MKDIR_ARGS $(dirname ${{ inputs.into-path }}/"${reference_file}" ) ; + if [[ "$reference_file" == Lib/test/* ]] ; then + # update patches with lib_updater tool from #6089 + ${{ inputs.into-path }}/scripts/lib_updater.py --from ${{ inputs.into-path }}/"${reference_file}" --to ${{ inputs.from-path }}/"${reference_file}" -o ${{ inputs.into-path }}/"${reference_file}" || \ + printf "::warning file='%s',title='integration failure':: Could not integrate file for Cpython %s on %s.\n" "${reference_file}" '${PYTHON_VERSION}' '${{ runner.os }}' ; + else + cp -vf ${{ inputs.from-path }}/"${reference_file}" ${{ inputs.into-path }}/"${reference_file}" || printf "::warning file='%s',title='integration failure':: Could not integrate file for Cpython %s on %s.\n" "${reference_file}" '${PYTHON_VERSION}' '${{ runner.os }}' ; + fi + done ; + printf "\n::endgroup::\n\n" ; diff --git a/.github/actions/CI-5974-Test-RustPython-Integration/action.yaml b/.github/actions/CI-5974-Test-RustPython-Integration/action.yaml new file mode 100644 index 00000000000..9ca4e30c25a --- /dev/null +++ b/.github/actions/CI-5974-Test-RustPython-Integration/action.yaml @@ -0,0 +1,348 @@ +--- +name: 'RustPython Smoke-Testing' +description: 'Smoke-Test Integrated Reference Implementation tests' +author: 'Mr. Walls' +branding: + icon: 'check-circle' + color: 'black' +inputs: + override-working-dir: + description: | + Path to integrated RustPython clone to smoke test. Default is 'rustpython' + required: true + default: ${{ github.server_url == 'https://github.com' && 'rustpython' || '' }} + override-rustpython-path: + description: | + override value for path to the Python Lib. The default is to use the value of the environment + variable 'RUSTPYTHONPATH'. Most users will find the default 'Lib' sufficient. + required: true + test-files: + description: | + List of paths to CPython Test files from source to destination. Default is 'Lib/test/*.py' + required: true + default: 'Lib/test/*.py' + python-version: + description: | + The Cpython version (e.g., any valid release or tag, 3.11, 3.12, 3.13) to override. + The default is to use the value of the environment variable 'PYTHON_VERSION'. + default: '3.13' + required: true + max-test-time: + description: | + The max time in seconds per test module file run before aborting a test attempt. The default + is deliberately short at a value of 30 seconds to keep total run-time down. + default: '30' + required: true + +# TODO: add verification steps + +runs: + using: composite + steps: + - id: output_python + env: + PYTHON_VERSION_INPUT: ${{ inputs.python-version }} + OVERRIDE_RUSTPYTHONPATH_INPUT: ${{ inputs.override-rustpython-path }} + name: "Detect Python" + if: ${{ !cancelled() && inputs.test-files != '' }} + shell: bash + run: | + printf "%s\n" "::group::detect-python-env" + if [[ -n $PYTHON_VERSION_INPUT ]]; then + printf "python-version=%s\n" "${PYTHON_VERSION_INPUT}" >> "$GITHUB_OUTPUT" + PYTHON_VERSION=${PYTHON_VERSION_INPUT} + else + printf "python-version=%s\n" "${PYTHON_VERSION}" >> "$GITHUB_OUTPUT" + fi + if [[ -n $OVERRIDE_RUSTPYTHONPATH_INPUT ]]; then + printf "override-rustpython-path=%s\n" "${OVERRIDE_RUSTPYTHONPATH_INPUT}" >> "$GITHUB_OUTPUT" + OVERRIDE_RUSTPYTHONPATH=${OVERRIDE_RUSTPYTHONPATH_INPUT} + else + printf "override-rustpython-path=%s\n" "${RUSTPYTHONPATH:-Lib}" >> "$GITHUB_OUTPUT" + OVERRIDE_RUSTPYTHONPATH="${RUSTPYTHONPATH:-Lib}" + fi + printf "%s\n" "PYTHON_VERSION=${PYTHON_VERSION}" >> "$GITHUB_ENV" + printf "%s\n" "OVERRIDE_RUSTPYTHONPATH=${OVERRIDE_RUSTPYTHONPATH}" >> "$GITHUB_ENV" + printf "Targeting Cpython %s on %s.\n" '${PYTHON_VERSION}' '${{ runner.os }}' ; + printf "%s\n" "::endgroup::" + - name: "Check Cargo Setup" + id: output_cargo_args + shell: bash + run: | + if [[ -n $CARGO_ARGS ]]; then + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then printf "::debug::CARGO_ARGS already set to '%s'\n" "${CARGO_ARGS}" ; fi ; + # e.g., CARGO_ARGS=${CARGO_ARGS} + else + CARGO_ARGS="--release" ; + if [[ "$RUNNER_DEBUG" == "true" ]] || [[ "$ACTIONS_STEP_DEBUG" == "true" ]]; then printf "::debug::CARGO_ARGS initialized to '%s'\n" "${CARGO_ARGS}" ; fi ; + fi ; + printf "%s\n" "CARGO_ARGS=${CARGO_ARGS}" >> "$GITHUB_ENV" ; + - name: "Prepare Artifact Name" + id: output_artifact_name + if: ${{ !cancelled() }} + shell: bash + run: | + printf "%s\n" "TEST_STEP_SUMMARY=CPython-Summary-Artifact-${{ runner.os }}-${PYTHON_VERSION}.md" >> "$GITHUB_ENV" + - id: store_old_path + if: ${{ !cancelled() }} + shell: bash + run: | + cd ${PWD:-.} ; + export OLD_PWD=$(pwd) ; # only local use for bootstrap + printf "initial-path=%s\n" "${OLD_PWD}" >> "$GITHUB_OUTPUT" + - name: "Try Smoke Testing" + id: smoke_test + shell: bash + if: ${{ !cancelled() && inputs.test-files != '' }} + env: + INPUT_FILES: ${{ inputs.test-files }} + OS: ${{ runner.os }} + CONTEXT_PHRASE: 'for Cpython ${{ steps.output_python.outputs.python-version }} on ${{ runner.os }}' + SUBSHELL_TIMEOUT: ${{ inputs.max-test-time }} + run: | + # TODO: clean this up + # Custom timeout function (GH-5974 - because ulimit is restricted and windows can't ulimit at all) + run_with_timeout() { + local timeout=$1 + shift + "$@" & + local pid=$! + ( sleep "$timeout" && kill -HUP "$pid" 2>/dev/null ) & disown + # Send HUP signal after timeout + wait "$pid" + local status=$? + if [ $status -eq 0 ]; then + printf "::debug::%s\n" "Command completed successfully." + true ; # force success result + elif [ $status -eq 143 ]; then + printf "::warning title='Timeout'::%s\n" "The command \`$@\` ${CONTEXT_PHRASE} was terminated due to timeout." + false ; + else + printf "%s\n" "The command failed with status $status ${CONTEXT_PHRASE}." + false ; + fi ; + } + + export -f run_with_timeout ; + # Usage + # run_with_timeout 360 your_command_here + cd ${{ inputs.override-working-dir }} || exit 13 ; + printf "%s\n\n" "# CPython ${PYTHON_VERSION} Results" > "${TEST_STEP_SUMMARY}" ; + for reference_file in ${INPUT_FILES}; do + if [[ ( -f "${reference_file}" ) ]] ; then + # See https://devguide.python.org/testing/run-write-tests + # Heuristic: "if some module does not have unittest.main(), then most likely it does not support direct invocation." + if grep -qF "unittest.main()" "${reference_file}" 2>/dev/null ; then + printf "Now Testing '%s'\n" "${reference_file}" + # vars for subshell but not for workflow + export REF_FILE_NAME=$(basename "${reference_file}") ; + printf "::group::%s\n" "${REF_FILE_NAME}" ; + # TODO: test with cpython first for baseline + # TODO: add to list of files that need additional prep due to hanging or unexpected failures that need to be removed + # Execute the testing command in a subshell + time ( + export RUSTPYTHONPATH=${OVERRIDE_RUSTPYTHONPATH:-Lib} ; + run_with_timeout ${SUBSHELL_TIMEOUT} cargo run $CARGO_ARGS -- ${RUSTPYTHONPATH:-Lib}/test/"${REF_FILE_NAME}" || RAW_COPY_OUTCOME='failing' + if [[ ( -n ${RAW_COPY_OUTCOME} ) ]] ; then + printf "::warning file='%s',title='test-warning':: Could not copy file %s unmodified, and pass tests %s.\n" "${reference_file}" "${reference_file}" "${CONTEXT_PHRASE}" ; + ( + run_with_timeout ${SUBSHELL_TIMEOUT} cargo run $CARGO_ARGS -- ./scripts/fix_test.py --path ${RUSTPYTHONPATH:-Lib}/test/"${REF_FILE_NAME}" || FIX_COPY_OUTCOME='unfixed' ; + ) ; + if [[ ( -n ${FIX_COPY_OUTCOME} ) ]] ; then + printf "::error file='%s',title='testing-failure':: Could not copy and auto-fix tests %s.\n" "${reference_file}" "${CONTEXT_PHRASE}" >&2 ; + # reset broken integration to last rustpython copy + git restore --ignore-unmerged --worktree --staged "${reference_file}" || : ; + git checkout -f --ignore-unmerged -- "${reference_file}" || : ; + # TODO: validate and conditionally set + FIX_COPY_OUTCOME="reverted" + else + FIX_COPY_OUTCOME="fixed" + RAW_COPY_OUTCOME="incompatible" + fi ; + else + FIX_COPY_OUTCOME="skipped" + RAW_COPY_OUTCOME="compatible" + fi ; + if [ -n ${RAW_COPY_OUTCOME} ]; then + if [[ "${RAW_COPY_OUTCOME}" == "compatible" ]] ; then + # printf "%s\n" ":ballot_box_with_check: Directly copying the test file \`${reference_file}\` is ${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + printf "## %s\n| Direct copy | :ballot_box_with_check: %s |\n" "\`${reference_file}\`" "${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + else + # printf "%s\n" ":black_square_button: Directly copying the test file \`${reference_file}\` is ${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + printf "## %s\n| Direct copy | :black_square_button: %s |\n" "\`${reference_file}\`" "${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + fi ; + if [ -n ${FIX_COPY_OUTCOME} ]; then + if [[ "${FIX_COPY_OUTCOME}" == "fixed" ]] ; then + # printf "%s\n\n" " :ballot_box_with_check: Copying and Auto-fixing the test file \`${reference_file}\` was successful" >> "${TEST_STEP_SUMMARY}" ; + printf "| Auto‑fix | :ballot_box_with_check: %s |\n\n" "successful" >> "${TEST_STEP_SUMMARY}" ; + else + # printf "%s\n\n" " :grey_exclamation: Copying and Auto-fixing the test file \`${reference_file}\` was ${FIX_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + printf "| Auto‑fix | :grey_exclamation: %s |\n\n" "${FIX_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + fi ; # end auto-fix + else + printf "\n" >> "${TEST_STEP_SUMMARY}" ; # extra space + fi ; + fi ; # end copy + printf "\n---\n%s Outcome:\n\tDirectly:%s\n\tAuto-Fix:%s\n\n" "${REF_FILE_NAME}" "${RAW_COPY_OUTCOME}" "${FIX_COPY_OUTCOME}" ; + printf "FIX_COPY_%s_OUTCOME=%s\n" "${REF_FILE_NAME}" "${FIX_COPY_OUTCOME}" >> "$GITHUB_ENV" ; + printf "RAW_COPY_%s_OUTCOME=%s\n" "${REF_FILE_NAME}" "${RAW_COPY_OUTCOME}" >> "$GITHUB_ENV" ; + # should dump a diff or something here + printf "\n\n" ; + unset RUSTPYTHONPATH 2>/dev/null || : ; + ) ; + # cleanup temp env + unset FIX_COPY_OUTCOME 2>/dev/null || : ; + unset RAW_COPY_OUTCOME 2>/dev/null || : ; + unset REF_FILE_NAME 2>/dev/null || : ; + printf "\n::endgroup::\n" ; + wait ; + else + # TODO: else can not be run directly and needs to be invoked with -m unittest -v test. + # TODO: cleanup this regular expression for edge-cases + if grep -qE "^[^cC]*([cC]lass)\s*(.+)(Test)" "${reference_file}" 2>/dev/null ; then + printf "Now Testing test-cases in '%s'\n" "${reference_file}" + # vars for subshell but not for workflow + export REF_TEST_NAME=$(basename -s ".py" "${reference_file}") ; + printf "%s\n" "Selected testcase test.${REF_TEST_NAME}" + printf "::group::%s\n" "${REF_FILE_NAME}" ; + # TODO: test with cpython first for baseline + # TODO: add to list of files that need additional prep due to hanging or unexpected failures that need to be removed + # Execute the testing command in a subshell + time ( + export RUSTPYTHONPATH=${OVERRIDE_RUSTPYTHONPATH:-Lib} ; + run_with_timeout ${SUBSHELL_TIMEOUT} cargo run $CARGO_ARGS -- -m unittest -v test.${REF_TEST_NAME} || RAW_COPY_OUTCOME='failing' + if [[ ( -n ${RAW_COPY_OUTCOME} ) ]] ; then + printf "::warning file='%s',title='test-warning':: Could not copy file %s unmodified, and pass tests %s.\n" "${reference_file}" "${reference_file}" "${CONTEXT_PHRASE}" ; + ( + run_with_timeout ${SUBSHELL_TIMEOUT} cargo run $CARGO_ARGS -- ./scripts/fix_test.py --path ${RUSTPYTHONPATH:-Lib}/test/"${REF_FILE_NAME}".py || FIX_COPY_OUTCOME='unfixed' ; + ) ; + if [[ ( -n ${FIX_COPY_OUTCOME} ) ]] ; then + printf "::error file='%s',title='testing-failure':: Could not copy and auto-fix tests for file.\n" "${reference_file}" >&2 ; + # reset broken integration to last rustpython copy + git restore --ignore-unmerged --worktree --staged "${reference_file}" || : ; + git checkout -f --ignore-unmerged -- "${reference_file}" || : ; + # TODO: validate and conditionally set + FIX_COPY_OUTCOME="reverted" + else + FIX_COPY_OUTCOME="fixed" + RAW_COPY_OUTCOME="incompatible" + fi ; + else + FIX_COPY_OUTCOME="skipped" + RAW_COPY_OUTCOME="compatible" + fi ; + if [ -n ${RAW_COPY_OUTCOME} ]; then + if [[ "${RAW_COPY_OUTCOME}" == "compatible" ]] ; then + # printf "%s\n" ":ballot_box_with_check: Directly copying the test file \`${reference_file}\` is ${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + printf "## %s\n| Direct copy | :ballot_box_with_check: %s |\n" "\`${reference_file}\`" "${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + else + # printf "%s\n" ":black_square_button: Directly copying the test file \`${reference_file}\` is ${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + printf "## %s\n| Direct copy | :black_square_button: %s |\n" "\`${reference_file}\`" "${RAW_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + fi ; + if [ -n ${FIX_COPY_OUTCOME} ]; then + if [[ "${FIX_COPY_OUTCOME}" == "fixed" ]] ; then + # printf "%s\n\n" " :ballot_box_with_check: Copying and Auto-fixing the test file \`${reference_file}\` was successful" >> "${TEST_STEP_SUMMARY}" ; + printf "| Auto‑fix | :ballot_box_with_check: %s |\n\n" "successful" >> "${TEST_STEP_SUMMARY}" ; + else + # printf "%s\n\n" " :grey_exclamation: Copying and Auto-fixing the test file \`${reference_file}\` was ${FIX_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + printf "| Auto‑fix | :grey_exclamation: %s |\n\n" "${FIX_COPY_OUTCOME}" >> "${TEST_STEP_SUMMARY}" ; + fi ; # end auto-fix + else + printf "\n" >> "${TEST_STEP_SUMMARY}" ; # extra space + fi ; + fi ; # end copy + printf "\n---\n%s Outcome:\n\tDirectly:%s\n\tAuto-Fix:%s\n\n" "${REF_TEST_NAME}" "${RAW_COPY_OUTCOME}" "${FIX_COPY_OUTCOME}" ; + wait ; # used to force boundary for std out read/write race in UI + printf "FIX_COPY_%s.py_OUTCOME=%s\n" "${REF_TEST_NAME}" "${FIX_COPY_OUTCOME}" >> "$GITHUB_ENV" ; + printf "RAW_COPY_%s.py_OUTCOME=%s\n" "${REF_TEST_NAME}" "${RAW_COPY_OUTCOME}" >> "$GITHUB_ENV" ; + # should dump a diff or something here + printf "\n\n" ; + unset RUSTPYTHONPATH 2>/dev/null || : ; + ) ; + # cleanup temp env + unset FIX_COPY_OUTCOME 2>/dev/null || : ; + unset RAW_COPY_OUTCOME 2>/dev/null || : ; + unset REF_TEST_NAME 2>/dev/null || : ; + printf "\n::endgroup::\n" ; + wait ; + else + printf "\nNow Skipping '%s'\n\n" "${reference_file}" ; + # printf "%s\n" ":grey_exclamation: Directly copying the filepath \`${reference_file}\` was inconclusive (_testing and validation skipped_)." >> "${TEST_STEP_SUMMARY}" ; + printf "## %s\n| Direct copy | :grey_exclamation: %s |\n" "\`${reference_file}\`" "inconclusive" >> "${TEST_STEP_SUMMARY}" ; + printf "\n" >> "${TEST_STEP_SUMMARY}" ; # extra space + fi ; + fi ; # TODO: else can not be run directly and needs to be invoked with -m unittest -v test. + fi ; + done + cat <"${TEST_STEP_SUMMARY}" >> "$GITHUB_STEP_SUMMARY" ; + cd ${{ steps.store_old_path.outputs.initial-path }} || exit 15 ; + - name: Format GitHub Actions environment file to JSON + if: ${{ always() }} + shell: bash + run: | + # Define patterns for variables to include (allowlist approach) + INCLUDE_PATTERNS=( + '^FIX_COPY_.*_OUTCOME=' + '^RAW_COPY_.*_OUTCOME=' + '^PYTHON_VERSION=' + '^TEST_STEP_SUMMARY=' + '^CARGO_ARGS=' + '^OVERRIDE_RUSTPYTHONPATH=' + '^RUNNER_OS=' + '^SUBSHELL_TIMEOUT=' + '^GITHUB_SHA=' + ) + # Define patterns for sensitive variables to always exclude (denylist) + EXCLUDE_PATTERNS=( + 'TOKEN' + 'SECRET' + 'PASSWORD' + 'KEY' + 'CREDENTIAL' + 'AUTH' + '_PWD' + 'GITHUB_TOKEN' + ) + # Filter GITHUB_ENV to include only safe, relevant variables + # Initialize a JSON object + echo "{" > formatted_env.json || true ; + # Filter GITHUB_ENV to include only safe, relevant variables + grep -E "$(IFS='|'; echo "${INCLUDE_PATTERNS[*]}")" "$GITHUB_ENV" | \ + grep -viE "$(IFS='|'; echo "${EXCLUDE_PATTERNS[*]}")" | \ + while IFS='=' read -r key value; do + # Append each key-value pair to the JSON file + # Escape characters that need to be escaped in JSON + escaped_key=$(echo "$key" | jq -R @json) + escaped_value=$(echo "$value" | jq -R @json) + echo " $escaped_key: $escaped_value," >> formatted_env.json + done ; + # Remove the trailing comma and close the JSON object + sed -i '$ s/,$//' formatted_env.json + echo "}" >> formatted_env.json + # Verify the output contains data + if [[ -s formatted_env.json ]] && jq -e 'length > 0' formatted_env.json >/dev/null 2>&1; then + echo "::debug::Successfully created filtered environment JSON with $(jq 'length' formatted_env.json) variables" + else + if [[ -s formatted_env.json ]] ; then + echo "::debug::Incompletely created environment JSON with unknown variables" + else + echo "::warning::No matching environment variables found to export" + echo '{}' > formatted_env.json + fi ; + fi ; + - name: Upload JSON as artifact + if: ${{ !cancelled() }} + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: formatted-env-${{ runner.os }}-${{ inputs.python-version }}-${{ github.sha }} + path: formatted_env.json + if-no-files-found: ignore + compression-level: 9 + retention-days: 1 + overwrite: true + - name: Post-Clean + id: post-bootstrap + run: | + exit 0 ; # don't break CI on regression + if: ${{ always() }} + shell: bash diff --git a/.github/workflows/Check_Tests.yml b/.github/workflows/Check_Tests.yml new file mode 100644 index 00000000000..a1b15a1202b --- /dev/null +++ b/.github/workflows/Check_Tests.yml @@ -0,0 +1,176 @@ +--- +name: CI-5974 +description: "Continuous Integration workflow for GHI 5974." +run-name: Prototyping integration tests ${{ github.ref_name }} +# +# Jobs included: +# - TEST-5974: Tests Integration with CPython Tests across Python versions and OSes +# +# Required Secrets: +# NONE +# +# WORK IN PROGRESS +# search for "TODO" in file for more details on what is still un-implemented + + +on: # yamllint disable-line rule:truthy + push: + branches: ["**"] # matches any branch + tags: ["v*"] + workflow_dispatch: + inputs: + python-version: + description: 'Target python version to target. Expected format: X.Y (e.g., 3.14)' + type: string + default: "3.14" + subshell-timeout: + description: 'Max time in seconds before marking a subshell as hung (per-subshell).' + type: int + default: 30 + +# Declare default permissions as none. +permissions: {} + +env: + #define cargo args like in cron + CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,ssl,jit + # Define Python versions at the top level -- Expected format: X.Y (e.g., 3.14) + PYTHON_DEFAULT: "${{ inputs.python-version || vars.PYTHON_DEFAULT || '3.14' }}" + PYTHON_OLD_MIN: "${{ vars.PYTHON_OLD_MIN || '3.9' }}" # For Oldest Python versions + PYTHON_OLD_EXTRA: "${{ vars.PYTHON_OLD_EXTRA || '3.12' }}" # For Older Python versions (Extra coverage) + PYTHON_EXPERIMENTAL: "${{ vars.PYTHON_EXPERIMENTAL || 'main' }}" # For future Python versions + # define how mush time before assuming the test has hung in seconds (parsed by sleep) + SUBSHELL_TIMEOUT: ${{ inputs.subshell-timeout || 30 }} + + +# TODO: coordinate with @moreal - to support initial use-case See RustPython/RustPython#5974 +# TODO: coordinate with @arihant2math - to really build out the migration/Reporting logic +# TODO: coordinate with @ShaharNaveh - see PR #6089 with RPAU + +jobs: + TEST-5974: + permissions: + actions: read + contents: read + statuses: write + packages: none + pull-requests: read + security-events: none + if: ${{ !cancelled() }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + continue-on-error: ${{ matrix.experimental }} + strategy: + max-parallel: 3 + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["${{ vars.PYTHON_OLD_MIN }}", "${{ vars.PYTHON_OLD_EXTRA }}", "${{ vars.PYTHON_DEFAULT }}", "${{ vars.PYTHON_EXPERIMENTAL }}"] + experimental: [true] + include: + - os: ubuntu-latest + python-version: "${{ vars.PYTHON_DEFAULT }}" + experimental: false + - os: macos-15 + python-version: "${{ vars.PYTHON_EXPERIMENTAL }}" + experimental: true + - os: windows-latest + python-version: "${{ vars.PYTHON_DEFAULT }}" + experimental: true + - os: macos-14 + python-version: "${{ vars.PYTHON_DEFAULT }}" + experimental: false + - os: windows-2025 + python-version: "${{ vars.PYTHON_DEFAULT }}" + experimental: true + outputs: + smoke_testing_status: ${{ steps.smoke_testing.outcome }} + env: + PYTHON_VERSION: ${{ matrix.python-version }} + steps: + - name: pre-checkout repository for actions + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + ref: ${{ github.ref || 'HEAD' }} + sparse-checkout: | + .github/actions + .github/actions/CI-5974-Fetch-RustPython + .github/actions/CI-5974-Fetch-CPython + .github/actions/CI-5974-Integrate-CPython + .github/actions/CI-5974-Test-RustPython-Integration + - name: Checkout RustPython repository on ${{ matrix.os }} + id: fetch-rpython + uses: ./.github/actions/CI-5974-Fetch-RustPython + with: + override-path: rustpython + override-rustpython-path: Lib + override-repository: 'RustPython/RustPython' + override-ref: main # Hint: could be changed to ${{ github.ref }} + - name: Fetch Reference Cpython ${{ matrix.python-version }} on ${{ matrix.os }} + id: fetch-cpython + uses: ./.github/actions/CI-5974-Fetch-CPython + with: + # Define the reference files to pull as a file glob pattern (defaults to ONLY tests) + match: "Lib/test/*.py Lib/test/**/*.py" + # ignore: "Lib/test/*.pyc" + python-version: ${{ matrix.python-version }} + override-path: cpython + override-cpython-lib-path: 'Lib/test' + # override-rustpython-path: ${{ steps.fetch-rpython.outputs.rustpython-lib-path }} + override-repository: 'python/cpython' + - name: "Integrate Cpython Test file" + id: merge_theirs + uses: ./.github/actions/CI-5974-Integrate-CPython + if: ${{ !cancelled() }} + with: + from-path: cpython + into-path: rustpython + files: | + ${{ steps.fetch-cpython.outputs.files }} + python-version: ${{ env.PYTHON_VERSION }} + - id: output_python + name: "bootstrap Python" + if: ${{ !cancelled() }} + shell: bash + run: | + printf "%s\n" "::group::bootstrap-python-env" + printf "python-version=%s\n" "${{ matrix.python-version }}" >> "$GITHUB_OUTPUT" + printf "Configured Cpython %s on %s.\n" '${{ matrix.python-version }}' '${{ matrix.os }}' ; + printf "PYTHON_VERSION=%s\n" "${{ matrix.python-version }}" >> "$GITHUB_ENV" + printf "%s\n" "::endgroup::" + - name: Try Smoke Testing + id: smoke_testing + uses: ./.github/actions/CI-5974-Test-RustPython-Integration + with: + override-working-dir: rustpython + # override-rustpython-path: 'Lib' + test-files: | + ${{ steps.fetch-cpython.outputs.files }} + python-version: ${{ env.PYTHON_VERSION }} + max-test-time: ${{ env.SUBSHELL_TIMEOUT }} # seconds + - name: Format GitHub Actions environment file to JSON + id: reformat_json + if: ${{ always() }} + shell: bash + run: | + echo '{}' > formatted_env.json # Initialize an empty JSON file + jq -Rn 'inputs | split("=") | { (.[0]): .[1] }' < "$GITHUB_ENV" | jq -s 'add' > formatted_env.json + + - name: Upload JSON as artifact + if: ${{ !cancelled() }} + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: formatted-env + path: formatted_env.json + if-no-files-found: ignore + compression-level: 9 + retention-days: 1 + overwrite: true + + - name: Post-Process + id: summarize + if: ${{ always() }} + shell: bash + run: | + exit 0 ; # don't break CI on regression